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.BizStatus; import jnpf.limsEntity.LimsQingyandanEntity; import jnpf.limsEntity.LimsSignEntity; import jnpf.limsEntity.LimsSignUsageTarget; import jnpf.limsEntity.SignOpType; import jnpf.limsService.LimsQingyandanService; 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.RestController; import java.util.Collections; import java.util.Map; @Slf4j @Tag(name = "lims请验单接口", description = "请验单提交 / 撤回 / 废弃 / 编辑") @RestController @RequestMapping("/lims/biz") public class LimsQingyandanController { @Autowired private LimsQingyandanService limsQingyandanService; @Autowired private LimsSignService limsSignService; /** * 成品请验单提交 -> 复制数据到取样表 */ @Transactional(rollbackFor = Exception.class) @Operation(summary = "成品请验单提交") @PostMapping("/qingyandan/submit") public R qingyandanSubmit(@RequestBody LimsQingyandanEntity formData) { LimsQingyandanEntity qingyandan = limsQingyandanService.findByFormData(formData); if (qingyandan == null) { return R.error("请验单数据不存在"); } BizStatus currentStatus = BizStatus.fromCode(qingyandan.getBizStatus()); if (currentStatus != BizStatus.CREATED && currentStatus != BizStatus.CANCELLED && currentStatus != BizStatus.REJECTED) { return R.error("当前状态[" + currentStatus.getLabel() + "]不允许提交"); } LimsSignEntity sign; try { sign = requireQingyandanSign(qingyandan, formData, SignOpType.QINGYANDAN_SUBMIT); } catch (DataException e) { return R.error(e.getMessage()); } limsQingyandanService.submit(qingyandan); claimQingyandanSignUsageOnly(sign, SignOpType.QINGYANDAN_SUBMIT, qingyandan); return R.success("提交成功"); } /** * 请验单操作:撤回 / 废弃 */ @Transactional(rollbackFor = Exception.class) @Operation(summary = "请验单操作(撤回/废弃)") @PostMapping("/qingyandan/action") public R qingyandanAction(@RequestBody Map params) { if (params == null) { return R.error("请求参数不能为空"); } String action = params.get("action"); String liushuihao = params.get("jianyan_liushuihao"); if (liushuihao == null || liushuihao.isEmpty()) { return R.error("检验流水号不能为空"); } LimsQingyandanEntity qingyandan = limsQingyandanService.findByLiushuihao(liushuihao); if (qingyandan == null) { return R.error("请验单数据不存在"); } BizStatus currentStatus = BizStatus.fromCode(qingyandan.getBizStatus()); switch (action) { case "cancel": if (currentStatus != BizStatus.CONFIRMED) { return R.error("只有已提交状态的请验单才能撤回"); } LimsSignEntity cancelSign; try { cancelSign = requireQingyandanActionSign( qingyandan, params, SignOpType.QINGYANDAN_CANCEL); } catch (DataException e) { return R.error(e.getMessage()); } if (!limsQingyandanService.cancel(qingyandan)) { return R.error("请验单状态已变更,撤回未生效"); } recordQingyandanSignUsage( cancelSign, SignOpType.QINGYANDAN_CANCEL, qingyandan); return R.success("撤回成功"); case "delete": if (currentStatus != BizStatus.CREATED && currentStatus != BizStatus.CANCELLED && currentStatus != BizStatus.REJECTED) { return R.error("只有已创建或已撤回状态的请验单才能废弃"); } limsQingyandanService.delete(qingyandan); return R.success("废弃成功"); default: return R.error("不支持的操作: " + action); } } /** * 请验单编辑 */ @Transactional(rollbackFor = Exception.class) @Operation(summary = "请验单编辑") @PostMapping("/qingyandan/edit") public R qingyandanEdit(@RequestBody LimsQingyandanEntity formData) { LimsQingyandanEntity qingyandan = limsQingyandanService.findByFormData(formData); if (qingyandan == null) { return R.error("请验单数据不存在"); } BizStatus currentStatus = BizStatus.fromCode(qingyandan.getBizStatus()); if (currentStatus != BizStatus.CREATED && currentStatus != BizStatus.CANCELLED && currentStatus != BizStatus.REJECTED) { return R.error("当前状态[" + currentStatus.getLabel() + "]不允许编辑"); } LimsSignEntity sign; try { sign = requireQingyandanSign(qingyandan, formData, SignOpType.QINGYANDAN_SUBMIT); } catch (DataException e) { return R.error(e.getMessage()); } limsQingyandanService.edit(qingyandan, formData); claimQingyandanSignUsageOnly(sign, SignOpType.QINGYANDAN_SUBMIT, qingyandan); return R.success("编辑成功"); } private LimsSignEntity requireQingyandanSign(LimsQingyandanEntity persisted, LimsQingyandanEntity request, SignOpType opType) { String signId = request == null ? null : request.getBizSign(); if (signId == null || signId.trim().isEmpty()) { throw new DataException("请添加电子签名"); } signId = signId.trim(); persisted.setBizSign(signId); if (request != null) { request.setBizSign(signId); } return limsSignService.requireValidSign( signId, opType, persisted.getJianyanLiushuihao()); } private LimsSignEntity requireQingyandanActionSign(LimsQingyandanEntity persisted, Map params, SignOpType opType) { String signId = params.get("biz_sign"); if (signId == null || signId.trim().isEmpty()) { signId = params.get("id"); } if (signId == null || signId.trim().isEmpty()) { throw new DataException("请添加电子签名"); } return limsSignService.requireValidSign( signId.trim(), opType, persisted.getJianyanLiushuihao()); } /** * 认领签名并记一条 {@code SIGN_USED}。 * *

🔴 只剩撤回({@code QINGYANDAN_CANCEL})走这条路,一字未动:撤回是列表按钮场景, * 层 0 根本不覆盖(实测撤回只产生层 2 + 层 3 两条事件,没有层 0 伴随事件)。 * 一旦让它也改走只认领,那次签名就彻底没有任何记录。 * *

提交 / 编辑改走 {@link #claimQingyandanSignUsageOnly}——见那里的说明。 */ private void recordQingyandanSignUsage(LimsSignEntity sign, SignOpType opType, LimsQingyandanEntity qingyandan) { limsSignService.recordSignUsage(sign, opType, Collections.singletonList(qingyandan.getId()), Collections.singletonList(qingyandanUsageTarget(qingyandan))); } /** * 只做签名的原子认领,不发 {@code SIGN_USED} 事件(2026-07-31,D8)。 * *

提交 / 编辑走的是在线表单路径:表单保存本身已经产生层 0 事件,层 0 会据表单里的 * {@code biz_sign} 字段补写一条签名事件;这里再发一条 {@code SIGN_USED} 就是同一次签名两条证据。 * *

认领没有被删:它是「签名单次消费」的防重放护栏(ed4d908), * 去掉等于同一个签名可以反复使用。本次改动只是把「认领」和「发事件」拆开。 */ private void claimQingyandanSignUsageOnly(LimsSignEntity sign, SignOpType opType, LimsQingyandanEntity qingyandan) { limsSignService.claimSignUsageOnly(sign, opType, Collections.singletonList(qingyandan.getId()), Collections.singletonList(qingyandanUsageTarget(qingyandan))); } private LimsSignUsageTarget qingyandanUsageTarget(LimsQingyandanEntity qingyandan) { return LimsSignUsageTarget.builder() .targetTable("lims_qingyandan") .targetId(qingyandan.getId()) .bizType("QINGYANDAN") .bizModule("LIMS-请验单") .bizCode(qingyandan.getJianyanLiushuihao()) .build(); } }