刘光辉
昨天 bb638871a7fb692d80f1b7a758f991dc0879002c
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
445
446
447
448
449
450
451
452
453
454
package jnpf.limsController;
 
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.exception.DataException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import jnpf.limsEntity.JianyanJielun;
import jnpf.limsEntity.LimsJianceTaskAcceptParam;
import jnpf.limsEntity.LimsJianyanAssignItem;
import jnpf.limsEntity.LimsJianyanAssignParam;
import jnpf.limsEntity.LimsJianyanAuditFailParam;
import jnpf.limsEntity.LimsJianyanAuditItem;
import jnpf.limsEntity.LimsJianyanAuditOutcome;
import jnpf.limsEntity.LimsJianyanAuditParam;
import jnpf.limsEntity.LimsJianyanResultItem;
import jnpf.limsEntity.LimsJianyanResultParam;
import jnpf.limsEntity.LimsJianyanXiangEntity;
import jnpf.limsEntity.LimsSignEntity;
import jnpf.limsEntity.LimsSignUsageTarget;
import jnpf.limsEntity.LimsSubmitResultOutcome;
import jnpf.limsEntity.SignOpType;
import jnpf.limsService.LimsJianceTaskRemindService;
import jnpf.limsService.LimsJianceTaskService;
import jnpf.limsService.LimsJianyanXiangService;
import jnpf.limsService.LimsSignService;
import jnpf.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
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;
 
@Slf4j
@Tag(name = "lims检测任务接口", description = "接样后的班组任务处理:接收 / 分样 / 指派检验员 等")
@RestController
@RequestMapping("/lims/biz/jiance-task")
public class LimsJianceTaskController {
 
    @Autowired
    private LimsJianceTaskService limsJianceTaskService;
 
    @Autowired
    private LimsSignService limsSignService;
 
    @Autowired
    private LimsJianyanXiangService limsJianyanXiangService;
 
    @Autowired
    private LimsJianceTaskRemindService limsJianceTaskRemindService;
 
    /**
     * 样品分检:把请验单 submit 时创建的检验项(created + wei_fenjian)推进到 pending_assign,
     * 标记 fenjian_zhuangtai=fenjian,并回写操作员录入的 4 个分检字段。
     * 请求体:{
     *   "biz_sign": "<lims_sign 主键>",
     *   "zuhe_fenjian_liang": "...",   // 组合分检量,请求级共享(选填,传了才写)
     *   "zuhe_fenjian_danwei": "...",  // 组合分检单位,请求级共享(选填,传了才写)
     *   "data": [{ "id": "<检验项 f_id>", "fenjian_liang": "...", "fenjian_danwei": "...",
     *             "fenjian_fenzhuang_rongqi": "...", "fenjian_baocun_tiaojian": "..." }, ...]
     * }
     * biz_sign:lims_sign 主键,op_type=jiance_task_accept;签名仅作审计记录在 lims_sign,业务表不落 sign 列。
     * 组合三列(zuhe_id + zuhe_fenjian_liang + zuhe_fenjian_danwei)仅写入 data 中 shifou_zuhe_fenjian=yes 的行;
     * zuhe_id 由后端生成、整次请求共享一个,组合量 / 单位取上述请求级共享值。
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "样品分检")
    @PostMapping("/accept")
    public R<String> fenjian(@RequestBody LimsJianceTaskAcceptParam param) {
        if (param == null || param.getData() == null || param.getData().isEmpty()) {
            return R.error("请选择至少一条检验项");
        }
        if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
            return R.error("请添加电子签名");
        }
        LimsSignEntity sign;
        try {
            sign = limsSignService.requireValidSign(
                    param.getBizSign(), SignOpType.JIANCE_TASK_ACCEPT, null);
        } catch (DataException e) {
            return R.error(e.getMessage());
        }
        List<String> fenjianIds = param.getData().stream().map(d -> d.getId()).collect(Collectors.toList());
 
        List<LimsSignUsageTarget> affectedTargets;
        try {
            affectedTargets = limsJianceTaskService.fenjianSamples(param);
        } catch (DataException e) {
            // 分检字段(分检量 / 单位 / 容器 / 保存条件)行级必填校验失败走到这里
            return R.error(e.getMessage());
        }
        int affected = affectedTargets.size();
 
        if (affected <= 0) {
            return R.error("所选检验项不在待分检状态");
        }
        limsSignService.recordSignUsage(
                sign, SignOpType.JIANCE_TASK_ACCEPT, fenjianIds, affectedTargets);
        return R.success("已分检 " + affected + " 条");
    }
 
    /**
     * 检验任务"逐行"分配(支持每行不同的检验员 / 要求完成日期,支持改派)。
     * 请求体:{
     *   "biz_sign": "签名数据",
     *   "data": [
     *     // 普通检验项:分配给检验员
     *     {"id": "id1", "jiancha_ren": "user1", "wancheng_riqi": "2026-04-30"},
     *     // 外检项(shifou_waijian=yes):分配给外检单位
     *     {"id": "id2", "waijian_danwei": "XX检测所", "waijian_lururen": "张三"}
     *   ]
     * }
     * biz_sign 为本次分配的共享签名,仅审计记录在 lims_sign(业务表不落签名列)。
     * 允许 pending_assign 或 pending_test 的行被更新,成功后置为 pending_test。
     *
     * <p>校验下沉到 service:行的 shifou_waijian 由 service 预查后决定分支必填字段,
     * 不在 controller 层根据入参直接判断(避免把"是否外检"由前端决定)。
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "检验任务逐行分配")
    @PostMapping("/assign")
    public R<String> assign(@RequestBody LimsJianyanAssignParam param) {
        if (param == null || param.getData() == null || param.getData().isEmpty()) {
            return R.error("请选择至少一条检验任务");
        }
        if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
            return R.error("请添加电子签名");
        }
        for (int i = 0; i < param.getData().size(); i++) {
            LimsJianyanAssignItem item = param.getData().get(i);
            if (item == null || item.getId() == null || item.getId().isEmpty()) {
                return R.error("第 " + (i + 1) + " 条缺少任务 id");
            }
        }
 
        LimsSignEntity sign;
        try {
            sign = limsSignService.requireValidSign(
                    param.getBizSign(), SignOpType.JIANCE_TASK_ASSIGN, null);
        } catch (DataException e) {
            return R.error(e.getMessage());
        }
        List<String> assignIds = param.getData().stream().map(d -> d.getId()).collect(Collectors.toList());
 
        List<LimsSignUsageTarget> affectedTargets;
        try {
            affectedTargets = limsJianceTaskService.assignJianyanXiang(param);
        } catch (DataException e) {
            // 普通检验项缺 jiancha_ren / wancheng_riqi、或外检项缺 waijian_danwei /
            // waijian_lururen 等行级校验失败走到这里
            return R.error(e.getMessage());
        }
        int affected = affectedTargets.size();
        int total = param.getData().size();
 
        if (affected <= 0) {
            return R.error("所选任务已进入审核 / 已审核阶段,无法分配");
        }
        limsSignService.recordSignUsage(
                sign, SignOpType.JIANCE_TASK_ASSIGN, assignIds, affectedTargets);
        if (affected < total) {
            return R.success("已分配 " + affected + " 条," + (total - affected) + " 条因状态不符被跳过");
        }
        return R.success("已分配 " + affected + " 条");
    }
 
    /**
     * 检验任务批量录入结果。
     * 请求体:{
     *   "biz_sign": "签名数据",
     *   "data": [
     *     {"id": "id1", "jianyan_jieguo": "7.2", "jianyan_jielun": "compliant", "biz_files": ""},
     *     {"id": "id2", "jianyan_jieguo": "白色粉末", "jianyan_jielun": "compliant", "biz_files": "f1;f2"}
     *   ]
     * }
     * biz_sign 为本次录入的共享签名,仅审计记录在 lims_sign(业务表不落签名列)。
     * 只允许 jianyan_zhuangtai = pending_test 且 jiancha_ren = 当前用户 的行被更新,
     * 成功后状态置为 pending_audit。
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "检验任务批量录入结果")
    @PostMapping("/submit-result")
    public R<String> submitResult(@RequestBody LimsJianyanResultParam param) {
        if (param == null || param.getData() == null || param.getData().isEmpty()) {
            return R.error("请选择至少一条检验任务");
        }
        if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
            return R.error("请添加电子签名");
        }
        for (int i = 0; i < param.getData().size(); i++) {
            LimsJianyanResultItem item = param.getData().get(i);
            if (item == null || item.getId() == null || item.getId().isEmpty()) {
                return R.error("第 " + (i + 1) + " 条缺少任务 id");
            }
            if (item.getJianyanJieguo() == null || item.getJianyanJieguo().isEmpty()) {
                return R.error("第 " + (i + 1) + " 条缺少检验结果");
            }
            if (item.getJianyanJielun() == null || item.getJianyanJielun().isEmpty()) {
                return R.error("第 " + (i + 1) + " 条缺少检验结论");
            }
            try {
                JianyanJielun.fromCode(item.getJianyanJielun());
            } catch (IllegalArgumentException e) {
                return R.error("第 " + (i + 1) + " 条检验结论值非法:" + item.getJianyanJielun());
            }
        }
 
        LimsSignEntity sign;
        try {
            sign = limsSignService.requireValidSign(
                    param.getBizSign(), SignOpType.JIANCE_SUBMIT_RESULT, null);
        } catch (DataException e) {
            return R.error(e.getMessage());
        }
        List<String> submitIds = param.getData().stream().map(d -> d.getId()).collect(Collectors.toList());
 
        LimsSubmitResultOutcome outcome = limsJianceTaskService.submitJianyanResult(param);
        int affected = outcome.getAffected();
        int total = param.getData().size();
        String flowWarn = "";
        if (!outcome.getFlowLaunchFailed().isEmpty()) {
            List<String> failed = outcome.getFlowLaunchFailed().stream().distinct().collect(Collectors.toList());
            flowWarn = ";另有 " + failed.size() + " 条已登记结果未能发起异常调查流程(流水号:"
                    + String.join("、", failed) + "),请检查工作流引擎";
        }
        if (affected <= 0) {
            return R.error("所选任务不在待录入状态,或并非本人任务");
        }
        limsSignService.recordSignUsage(
                sign, SignOpType.JIANCE_SUBMIT_RESULT, submitIds, outcome.getAffectedTargets());
        if (affected < total) {
            return R.success("已提交 " + affected + " 条," + (total - affected) + " 条因状态不对或非本人任务被跳过" + flowWarn);
        }
        return R.success("已提交 " + affected + " 条" + flowWarn);
    }
 
    /**
     * 检验结果批量审核通过。
     * 请求体:{
     *   "biz_sign": "审核人签名",
     *   "data": [{"id": "id1"}, {"id": "id2"}]
     * }
     * biz_sign 为本次审核的共享签名,仅审计记录在 lims_sign(业务表不落签名列);
     * 审核人(renwu_shenheren)取当前登录用户 userId。
     * 允许 pending_audit 的行被更新,成功后状态置为 audited。
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "检验结果批量审核通过")
    @PostMapping("/audit-pass")
    public R<String> auditPass(@RequestBody LimsJianyanAuditParam param) {
        if (param == null || param.getData() == null || param.getData().isEmpty()) {
            return R.error("请选择至少一条检验任务");
        }
        if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
            return R.error("请添加电子签名");
        }
        for (int i = 0; i < param.getData().size(); i++) {
            LimsJianyanAuditItem item = param.getData().get(i);
            if (item == null || item.getId() == null || item.getId().isEmpty()) {
                return R.error("第 " + (i + 1) + " 条缺少任务 id");
            }
        }
 
        LimsSignEntity sign;
        try {
            sign = limsSignService.requireValidSign(
                    param.getBizSign(), SignOpType.JIANCE_AUDIT_PASS, null);
        } catch (DataException e) {
            return R.error(e.getMessage());
        }
        List<String> auditPassIds = param.getData().stream().map(d -> d.getId()).collect(Collectors.toList());
 
        LimsJianyanAuditOutcome outcome = limsJianceTaskService.passJianyanAudit(param);
        int affected = outcome.getAffected();
        int total = outcome.getTotal();
        if (affected <= 0) {
            return R.error(buildAuditOutcomeRejectMessage(outcome, "审核"));
        }
        limsSignService.recordSignUsage(
                sign, SignOpType.JIANCE_AUDIT_PASS, auditPassIds, outcome.getAffectedTargets());
        if (affected < total) {
            return R.success("已审核 " + affected + " 条," + (total - affected)
                    + " 条被跳过(" + buildSkippedReason(outcome) + ")");
        }
        return R.success("已审核 " + affected + " 条");
    }
 
    /**
     * 检验结果项目退回。
     * 请求体:{
     *   "biz_sign": "退回人签名",
     *   "note": "共享的退回原因",
     *   "data": [{"id": "id1"}, {"id": "id2"}]
     * }
     * biz_sign 共享(仅审计在 lims_sign,业务表不落签名列);note 共享 → 每行 tuihui_yuanyin;
     * 退回人(renwu_tuihuiren)取当前登录用户 userId。
     * 入口状态 audited,成功后置为 audit_rejected。
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "检验结果批量项目退回")
    @PostMapping("/audit-fail")
    public R<String> auditFail(@RequestBody LimsJianyanAuditFailParam param) {
        if (param == null || param.getData() == null || param.getData().isEmpty()) {
            return R.error("请选择至少一条检验任务");
        }
        if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
            return R.error("请添加电子签名");
        }
        if (param.getNote() == null || param.getNote().isEmpty()) {
            return R.error("请填写退回原因");
        }
        for (int i = 0; i < param.getData().size(); i++) {
            LimsJianyanAuditItem item = param.getData().get(i);
            if (item == null || item.getId() == null || item.getId().isEmpty()) {
                return R.error("第 " + (i + 1) + " 条缺少任务 id");
            }
        }
 
        LimsSignEntity sign;
        try {
            sign = limsSignService.requireValidSign(
                    param.getBizSign(), SignOpType.JIANCE_AUDIT_FAIL, null);
        } catch (DataException e) {
            return R.error(e.getMessage());
        }
        List<String> auditFailIds = param.getData().stream().map(d -> d.getId()).collect(Collectors.toList());
 
        LimsJianyanAuditOutcome outcome = limsJianceTaskService.failJianyanAudit(param);
        int affected = outcome.getAffected();
        int total = outcome.getTotal();
        if (affected <= 0) {
            return R.error(buildAuditOutcomeRejectMessage(outcome, "退回"));
        }
        limsSignService.recordSignUsage(
                sign, SignOpType.JIANCE_AUDIT_FAIL, auditFailIds, outcome.getAffectedTargets());
        if (affected < total) {
            return R.success("已退回 " + affected + " 条," + (total - affected)
                    + " 条被跳过(" + buildSkippedReason(outcome) + ")");
        }
        return R.success("已退回 " + affected + " 条");
    }
 
    /**
     * 全量被拒时,按 outcome 各分桶大小给出针对性原因。
     * 单一原因时直接说明,多原因并存时分类列出。audit-pass / audit-fail 共用。
     *
     * @param action "审核" 或 "退回" — 用于动词替换
     */
    private String buildAuditOutcomeRejectMessage(LimsJianyanAuditOutcome o, String action) {
        List<String> parts = new java.util.ArrayList<>();
        if (!o.getNotFoundIds().isEmpty()) {
            parts.add(o.getNotFoundIds().size() + " 条任务不存在或已删除");
        }
        if (!o.getWrongStatusIds().isEmpty()) {
            parts.add(o.getWrongStatusIds().size() + " 条任务状态不是「待审核」,无法" + action);
        }
        if (!o.getSelfRejectIds().isEmpty()) {
            parts.add(o.getSelfRejectIds().size() + " 条任务的录入人是您本人,不能" + action + "自己的任务");
        }
        if (!o.getNotInBanzuIds().isEmpty()) {
            parts.add(o.getNotInBanzuIds().size() + " 条任务所属班组您不是成员,无权" + action);
        }
        if (parts.isEmpty()) {
            return "没有可" + action + "的任务";
        }
        if (parts.size() == 1) {
            return parts.get(0);
        }
        return "无可" + action + "任务:" + String.join(";", parts);
    }
 
    /**
     * 单条撤回检验项结果:仅 pending_audit → cancelled。
     * 请求体:{ "id": "<lims_jianyan_xiang.f_id>", "biz_sign": "<lims_sign 主键>" }
     * biz_sign 是预先 /lims/biz/sign/verify 拿到的 sign_id,op_type=jiance_result_cancel,
     * data_id=本检验项 id。本接口会校验真实性 / op_type / data_id / 签名人。
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "撤回检验项结果")
    @PostMapping("/cancel-result")
    public R<String> cancelResult(@RequestBody Map<String, String> params) {
        String id = params == null ? null : params.get("id");
        String bizSign = params == null ? null : params.get("biz_sign");
        if (id == null || id.isEmpty()) {
            return R.error("缺少检验项 id");
        }
        if (bizSign == null || bizSign.isEmpty()) {
            return R.error("请添加电子签名");
        }
        LimsJianyanXiangEntity row = limsJianyanXiangService.getById(id);
        if (row == null || (row.getDeleteMark() != null && row.getDeleteMark() == 1)) {
            return R.error("当前检验项不存在或已删除");
        }
        LimsSignEntity sign;
        try {
            sign = limsSignService.requireValidSign(
                    bizSign, SignOpType.JIANCE_RESULT_CANCEL, id);
        } catch (DataException e) {
            return R.error(e.getMessage());
        }
        if (!limsJianyanXiangService.cancelResult(id)) {
            return R.error("当前检验项不在待审核状态,无法撤回");
        }
        LimsSignUsageTarget target = LimsSignUsageTarget.builder()
                .targetTable("lims_jianyan_xiang")
                .targetId(row.getId())
                .bizType("JIANCE_TASK")
                .bizModule("LIMS-检测任务")
                .bizCode(row.getJianyanLiushuihao())
                .build();
        limsSignService.recordSignUsage(sign, SignOpType.JIANCE_RESULT_CANCEL,
                Collections.singletonList(id), Collections.singletonList(target));
        return R.success("已撤回");
    }
 
    /** 部分成功时简短描述被跳过的原因,audit-pass / audit-fail 共用。 */
    private String buildSkippedReason(LimsJianyanAuditOutcome o) {
        List<String> parts = new java.util.ArrayList<>();
        if (!o.getNotFoundIds().isEmpty()) {
            parts.add("任务不存在 " + o.getNotFoundIds().size());
        }
        if (!o.getWrongStatusIds().isEmpty()) {
            parts.add("状态不符 " + o.getWrongStatusIds().size());
        }
        if (!o.getSelfRejectIds().isEmpty()) {
            parts.add("录入人是本人 " + o.getSelfRejectIds().size());
        }
        if (!o.getNotInBanzuIds().isEmpty()) {
            parts.add("非本班组 " + o.getNotInBanzuIds().size());
        }
        return parts.isEmpty() ? "未知原因" : String.join(",", parts);
    }
 
    /**
     * 手动触发"要求完成日期"提醒扫描(开发 / 紧急补发场景)。
     *
     * <p>正常生产由 cron 每天 8:00 自动触发;本端点为绕过 cron 的运维入口。
     *
     * @param dry true 仅日志不发消息、不写 remind_flags(开发期调试默认)
     * @return 各分支统计 {hit, sent, skipped}
     */
    @Operation(summary = "手动触发完成日期提醒扫描")
    @PostMapping("/remind/run-now")
    public R<Map<String, Map<String, Integer>>> remindRunNow(
            @RequestParam(value = "dry", defaultValue = "true") boolean dry) {
        Map<String, Map<String, Integer>> stats = limsJianceTaskRemindService.scanAndRemind(dry);
        return R.success(stats);
    }
}