package jnpf.limsService.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import jnpf.audit.model.AuditEventDTO; import jnpf.audit.sdk.AuditClient; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.audit.AuditConsts; import jnpf.base.UserInfo; import jnpf.limsEntity.BaogaoStatus; import jnpf.limsEntity.JianyanJielun; import jnpf.limsEntity.JianyanStatus; import jnpf.limsEntity.LimsBaogaoSubmitParam; import jnpf.limsEntity.LimsJianyanXiangEntity; import jnpf.limsEntity.LimsQingyandanEntity; import jnpf.limsEntity.bizlog.BizLogAction; import jnpf.limsEntity.bizlog.BizLogType; import jnpf.limsService.LimsJianyanXiangService; import jnpf.limsService.LimsQingyandanReportService; import jnpf.limsService.LimsQingyandanService; import jnpf.util.JsonUtil; import jnpf.util.UserProvider; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Objects; /** * 报告生命周期服务实现。报告已合并进 lims_qingyandan,所有动作按请验单 id 在该表的 * baogao_* 列上流转,不再读写 lims_qingyandan_report。 */ @Service public class LimsQingyandanReportServiceImpl implements LimsQingyandanReportService { @Autowired private LimsQingyandanService limsQingyandanService; @Autowired private LimsJianyanXiangService limsJianyanXiangService; @Autowired private LimsOperationalMessagePublisher operationalMessagePublisher; /** * 层 3 显式审计入口。**必须用 {@link ObjectProvider} 注入**:{@code audit.enabled=false} 时 * 整个自动配置类连同 AuditClient Bean 一起缺席,普通 @Autowired 会让本服务起不来。 */ @Autowired private ObjectProvider auditClientProvider; @Override public boolean generateReportData(String qingyandanId) { if (qingyandanId == null || qingyandanId.isEmpty()) { return false; } // 1. 源请验单(主键查)。不存在 / 已软删直接跳过(不抛异常,避免回滚上游的审核动作) LimsQingyandanEntity qingyandan = limsQingyandanService.getById(qingyandanId); if (qingyandan == null || (qingyandan.getDeleteMark() != null && qingyandan.getDeleteMark() == 1)) { return false; } // 幂等:baogao_zhuangtai 非空串即视为已生成(含已提交/审核/批准),跳过 if (qingyandan.getBaogaoZhuangtai() != null && !qingyandan.getBaogaoZhuangtai().isEmpty()) { return false; } // 2. 该请验单下所有未软删的检验项:判"全 audited" + 算 shifou_hege List xiangRows = loadXiangRowsByQingyandanId(qingyandanId); if (xiangRows.isEmpty() || hasUnauditedXiang(xiangRows)) { return false; } // 3. 在请验单行上落报告初始数据(仅当 baogao_zhuangtai 仍为空串,防并发) UpdateWrapper uw = new UpdateWrapper<>(); uw.eq("f_id", qingyandanId); uw.eq("baogao_zhuangtai", ""); uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1)); uw.set("baogao_zhuangtai", BaogaoStatus.CREATED.getCode()); uw.set("baogao_shifou_hege", computeShifouHege(xiangRows)); // 报告书编号首次生成后保持稳定(退回不清空),避免重新生成时编号被复用 if (qingyandan.getBaogaoBianhao() == null || qingyandan.getBaogaoBianhao().isEmpty()) { uw.set("baogao_bianhao", generateBaogaoBianhao(qingyandan.getChanpinBianma())); } return limsQingyandanService.update(null, uw); } /** 拿该请验单下所有未软删的检验项。不要用 select 限定列,JNPF 拦截器会把缺系统列的行整体置 null。 */ private List loadXiangRowsByQingyandanId(String qingyandanId) { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("qingyandan_id", qingyandanId); wrapper.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1)); return limsJianyanXiangService.list(wrapper); } /** 所有检验项(含外检)必须 audited 才就绪,任意一行不就绪即 true。 */ private boolean hasUnauditedXiang(List rows) { return rows.stream() .filter(Objects::nonNull) .anyMatch(r -> !JianyanStatus.AUDITED.getCode().equals(r.getJianyanZhuangtai())); } /** 任一检验项结论 non_compliant → "no",否则 "yes"。 */ private String computeShifouHege(List rows) { boolean anyNonCompliant = rows.stream() .filter(Objects::nonNull) .anyMatch(r -> JianyanJielun.NON_COMPLIANT.getCode().equals(r.getJianyanJielun())); return anyNonCompliant ? "no" : "yes"; } /** * 生成报告书编号:{chanpin_bianma}-{yyyyDDD}A{NN},例:CT001-2026005A01。 * NN 取同 (chanpin_bianma, yyyyDDD) 前缀的已分配编号数 + 1,2 位补零。 * 编号一经分配不再清空(退回保留),计数单调,避免复用。 */ private String generateBaogaoBianhao(String chanpinBianma) { String yyyyDDD = new SimpleDateFormat("yyyyDDD").format(new Date()); String prefix = (chanpinBianma == null ? "" : chanpinBianma) + "-" + yyyyDDD + "A"; QueryWrapper wrapper = new QueryWrapper<>(); wrapper.likeRight("baogao_bianhao", prefix); long n = limsQingyandanService.count(wrapper) + 1; return prefix + String.format("%02d", n); } @Override @AuditLog( action = "REPORT_GENERATE", label = "生成报告", bizType = "BAOGAO", bizCodeExpr = "#result ? #param.id : ''", targetTable = "lims_qingyandan", targetIdExpr = "#param.id", diff = true, entityClass = LimsQingyandanEntity.class ) public boolean submitReport(LimsBaogaoSubmitParam param) { UserInfo userInfo = UserProvider.getUser(); LimsQingyandanEntity report = limsQingyandanService.getById(param.getId()); if (report == null) { return false; } Date submittedAt = new Date(); UpdateWrapper uw = new UpdateWrapper<>(); uw.eq("f_id", param.getId()); // 仅 created 可被"生成报告",防并发 / 防重复点击 uw.eq("baogao_zhuangtai", BaogaoStatus.CREATED.getCode()); uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1)); uw.set("baogao_ren", userInfo.getUserId()); uw.set("baogao_riqi", param.getBaogaoRiqi()); uw.set("baogao_beizhu", param.getBeizhu()); uw.set("baogao_zhuangtai", BaogaoStatus.PENDING_AUDIT.getCode()); // 重新生成报告时清空上一次退回痕迹 uw.set("baogao_tuihui_yuanyin", null); uw.set("baogao_tuihui_ren", null); uw.set("baogao_tuihui_riqi", null); boolean updated = limsQingyandanService.update(null, uw); if (updated) { operationalMessagePublisher.reportPendingReview(report, userInfo, submittedAt); } return updated; } @Override @AuditLog( action = "AUDIT_PASS", label = "报告审核通过", bizType = "BAOGAO", bizCodeExpr = "#result ? #qingyandanId : ''", targetTable = "lims_qingyandan", targetIdExpr = "#qingyandanId", diff = true, entityClass = LimsQingyandanEntity.class ) public boolean auditPassReport(String qingyandanId) { UserInfo userInfo = UserProvider.getUser(); LimsQingyandanEntity report = limsQingyandanService.getById(qingyandanId); if (report == null || Objects.equals(report.getBaogaoRen(), userInfo.getUserId())) { return false; } Date reviewedAt = new Date(); UpdateWrapper uw = new UpdateWrapper<>(); uw.eq("f_id", qingyandanId); uw.eq("baogao_zhuangtai", BaogaoStatus.PENDING_AUDIT.getCode()); uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1)); uw.set("baogao_shenhe_ren", userInfo.getUserId()); uw.set("baogao_shenhe_riqi", reviewedAt); uw.set("baogao_zhuangtai", BaogaoStatus.AUDITED.getCode()); boolean updated = limsQingyandanService.update(null, uw); if (updated) { operationalMessagePublisher.reportPendingApproval(report, userInfo, reviewedAt); } return updated; } @Override @AuditLog( action = "APPROVE", label = "报告批准", bizType = "BAOGAO", bizCodeExpr = "#result ? #qingyandanId : ''", targetTable = "lims_qingyandan", targetIdExpr = "#qingyandanId", diff = true, entityClass = LimsQingyandanEntity.class ) public boolean approveReport(String qingyandanId) { UserInfo userInfo = UserProvider.getUser(); LimsQingyandanEntity report = limsQingyandanService.getById(qingyandanId); if (report == null || Objects.equals(report.getBaogaoRen(), userInfo.getUserId()) || Objects.equals(report.getBaogaoShenheRen(), userInfo.getUserId())) { return false; } UpdateWrapper uw = new UpdateWrapper<>(); uw.eq("f_id", qingyandanId); uw.eq("baogao_zhuangtai", BaogaoStatus.AUDITED.getCode()); uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1)); uw.set("baogao_pizhun_ren", userInfo.getUserId()); uw.set("baogao_pizhun_riqi", new Date()); uw.set("baogao_zhuangtai", BaogaoStatus.APPROVED.getCode()); return limsQingyandanService.update(null, uw); } @Override public boolean rejectReport(String qingyandanId, String tuihuiYuanyin) { UserInfo userInfo = UserProvider.getUser(); LimsQingyandanEntity qingyandan = limsQingyandanService.getById(qingyandanId); if (qingyandan == null || (qingyandan.getDeleteMark() != null && qingyandan.getDeleteMark() == 1)) { return false; } String currentStatus = qingyandan.getBaogaoZhuangtai(); boolean rejectable = BaogaoStatus.CREATED.getCode().equals(currentStatus) || BaogaoStatus.PENDING_AUDIT.getCode().equals(currentStatus) || BaogaoStatus.AUDITED.getCode().equals(currentStatus); if (!rejectable) { return false; } Date now = new Date(); List affectedItems = loadXiangRowsByQingyandanId(qingyandanId); // 重置报告阶段字段回初始(baogao_bianhao 保留,便于重新生成时沿用旧编号), // 写退回三件套;带状态守卫防并发。 UpdateWrapper uw = new UpdateWrapper<>(); uw.eq("f_id", qingyandanId); uw.eq("baogao_zhuangtai", currentStatus); uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1)); uw.set("baogao_zhuangtai", ""); uw.set("baogao_ren", null); uw.set("baogao_riqi", null); uw.set("baogao_beizhu", null); uw.set("baogao_shenhe_ren", null); uw.set("baogao_shenhe_riqi", null); uw.set("baogao_pizhun_ren", null); uw.set("baogao_pizhun_riqi", null); uw.set("baogao_tuihui_yuanyin", tuihuiYuanyin); uw.set("baogao_tuihui_ren", userInfo.getUserId()); uw.set("baogao_tuihui_riqi", now); if (!limsQingyandanService.update(null, uw)) { return false; } // 检验项按报告当前状态推回: // created → AUDIT_REJECTED(重新录入 → 复核 → 触发新一轮生成) // pending_audit / audited → PENDING_AUDIT(重新 audit-pass → 触发新一轮生成) String xiangTargetStatus = BaogaoStatus.CREATED.getCode().equals(currentStatus) ? JianyanStatus.AUDIT_REJECTED.getCode() : JianyanStatus.PENDING_AUDIT.getCode(); UpdateWrapper xw = new UpdateWrapper<>(); xw.eq("jianyan_liushuihao", qingyandan.getJianyanLiushuihao()); xw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1)); xw.set("jianyan_zhuangtai", xiangTargetStatus); limsJianyanXiangService.update(null, xw); recordRejectReportEvent(qingyandan.getJianyanLiushuihao(), qingyandanId, tuihuiYuanyin); operationalMessagePublisher.reportRejected( qingyandan, affectedItems, currentStatus, tuihuiYuanyin, userInfo, now); return true; } /** * 层 3 显式写一条「报告退回」审计事件。 * *

为什么走层 3 而不是方法级 {@code @AuditLog}:本事件的 bizCode 是 * {@code qingyandan.getJianyanLiushuihao()},而 {@code qingyandan} 是 {@link #rejectReport} * 方法体内 {@code getById} 查出来的**局部变量**——方法级 SpEL 只能取方法参数 * ({@code qingyandanId} / {@code tuihuiYuanyin})与 {@code #result},够不着它。 * *

{@code operationId} **本方法不传**,交 {@code AuditTxHolder} 单源补齐,理由同 * {@code LimsJianceTaskServiceImpl#recordAuditFailEvent}——同处也记了「原注释说留空」的勘误。 * 实测本事件的 operation_id 非空,且与同一批 AUDIT_FAIL 那次不同(两次操作各自成域)。 * *

extra 用 {@code reason} 键,与层 2 @AuditLog 的 {@code extraExpr="{'reason': …}"} 对齐, * 不沿用旧表的 {@code note}——否则同一 actionCode 在 audit_events 里出现两种键名。 */ private void recordRejectReportEvent(String jianyanLiushuihao, String qingyandanId, String tuihuiYuanyin) { auditClientProvider.ifAvailable(client -> client.record(AuditEventDTO.builder() .eventType(AuditConsts.TYPE_BIZ_ACTION) .actionCode(BizLogAction.RETURN.getCode()) .actionLabel("报告退回") .bizType(BizLogType.BAOGAO.getCode()) .bizCode(jianyanLiushuihao) .targetTable("lims_qingyandan") .targetId(qingyandanId) .sourceLayer(3) .extra(JsonUtil.getObjectToString(Collections.singletonMap("reason", tuihuiYuanyin))) .build())); } }