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<AuditClient> 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<LimsJianyanXiangEntity> xiangRows = loadXiangRowsByQingyandanId(qingyandanId);
|
if (xiangRows.isEmpty() || hasUnauditedXiang(xiangRows)) {
|
return false;
|
}
|
|
// 3. 在请验单行上落报告初始数据(仅当 baogao_zhuangtai 仍为空串,防并发)
|
UpdateWrapper<LimsQingyandanEntity> 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<LimsJianyanXiangEntity> loadXiangRowsByQingyandanId(String qingyandanId) {
|
QueryWrapper<LimsJianyanXiangEntity> 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<LimsJianyanXiangEntity> rows) {
|
return rows.stream()
|
.filter(Objects::nonNull)
|
.anyMatch(r -> !JianyanStatus.AUDITED.getCode().equals(r.getJianyanZhuangtai()));
|
}
|
|
/** 任一检验项结论 non_compliant → "no",否则 "yes"。 */
|
private String computeShifouHege(List<LimsJianyanXiangEntity> 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<LimsQingyandanEntity> 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<LimsQingyandanEntity> 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<LimsQingyandanEntity> 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<LimsQingyandanEntity> 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<LimsJianyanXiangEntity> affectedItems = loadXiangRowsByQingyandanId(qingyandanId);
|
// 重置报告阶段字段回初始(baogao_bianhao 保留,便于重新生成时沿用旧编号),
|
// 写退回三件套;带状态守卫防并发。
|
UpdateWrapper<LimsQingyandanEntity> 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<LimsJianyanXiangEntity> 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 显式写一条「报告退回」审计事件。
|
*
|
* <p>为什么走层 3 而不是方法级 {@code @AuditLog}:本事件的 bizCode 是
|
* {@code qingyandan.getJianyanLiushuihao()},而 {@code qingyandan} 是 {@link #rejectReport}
|
* 方法体内 {@code getById} 查出来的**局部变量**——方法级 SpEL 只能取方法参数
|
* ({@code qingyandanId} / {@code tuihuiYuanyin})与 {@code #result},够不着它。
|
*
|
* <p>{@code operationId} **本方法不传**,交 {@code AuditTxHolder} 单源补齐,理由同
|
* {@code LimsJianceTaskServiceImpl#recordAuditFailEvent}——同处也记了「原注释说留空」的勘误。
|
* 实测本事件的 operation_id 非空,且与同一批 AUDIT_FAIL 那次不同(两次操作各自成域)。
|
*
|
* <p>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()));
|
}
|
}
|