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": "", * "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 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 fenjianIds = param.getData().stream().map(d -> d.getId()).collect(Collectors.toList()); List 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。 * *

校验下沉到 service:行的 shifou_waijian 由 service 预查后决定分支必填字段, * 不在 controller 层根据入参直接判断(避免把"是否外检"由前端决定)。 */ @Transactional(rollbackFor = Exception.class) @Operation(summary = "检验任务逐行分配") @PostMapping("/assign") public R 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 assignIds = param.getData().stream().map(d -> d.getId()).collect(Collectors.toList()); List 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 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 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 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 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 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 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 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 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": "", "biz_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 cancelResult(@RequestBody Map 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 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); } /** * 手动触发"要求完成日期"提醒扫描(开发 / 紧急补发场景)。 * *

正常生产由 cron 每天 8:00 自动触发;本端点为绕过 cron 的运维入口。 * * @param dry true 仅日志不发消息、不写 remind_flags(开发期调试默认) * @return 各分支统计 {hit, sent, skipped} */ @Operation(summary = "手动触发完成日期提醒扫描") @PostMapping("/remind/run-now") public R>> remindRunNow( @RequestParam(value = "dry", defaultValue = "true") boolean dry) { Map> stats = limsJianceTaskRemindService.scanAndRemind(dry); return R.success(stats); } }