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.AuditConsts;
|
import jnpf.audit.sdk.annotation.AuditLog;
|
import jnpf.base.UserInfo;
|
import jnpf.limsEntity.FenjianZhuangtai;
|
import jnpf.limsEntity.JianyanStatus;
|
import jnpf.limsEntity.LimsFenjianItem;
|
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.LimsQingyandanEntity;
|
import jnpf.limsEntity.LimsSignUsageTarget;
|
import jnpf.limsEntity.LimsSubmitResultOutcome;
|
import jnpf.limsEntity.LimsJianyanXiangEntity;
|
import jnpf.limsEntity.bizlog.BizLogAction;
|
import jnpf.limsEntity.bizlog.BizLogType;
|
import jnpf.limsMapper.LimsJianyanXiangMapper;
|
import jnpf.limsMapper.LimsQingyandanMapper;
|
import jnpf.limsService.LimsJianceTaskService;
|
import jnpf.limsService.LimsQingyandanReportService;
|
import jnpf.limsService.LimsYichangDiaochaService;
|
import jnpf.permission.UserRelationApi;
|
import jnpf.permission.entity.UserRelationEntity;
|
import jnpf.constant.PermissionConst;
|
import jnpf.exception.DataException;
|
import jnpf.util.JsonUtil;
|
import jnpf.util.RandomUtil;
|
import jnpf.util.UserProvider;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.math.BigDecimal;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.Date;
|
import java.util.LinkedHashMap;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.stream.Collectors;
|
|
@Slf4j
|
@Service
|
public class LimsJianceTaskServiceImpl implements LimsJianceTaskService {
|
|
@Autowired
|
private LimsJianyanXiangMapper limsJianyanXiangMapper;
|
|
@Autowired
|
private LimsQingyandanMapper limsQingyandanMapper;
|
|
@Autowired
|
private UserRelationApi userRelationApi;
|
|
@Autowired
|
private LimsQingyandanReportService limsQingyandanReportService;
|
|
@Autowired
|
private LimsYichangDiaochaService limsYichangDiaochaService;
|
|
@Autowired
|
private LimsOperationalMessagePublisher operationalMessagePublisher;
|
|
/**
|
* 层 3 显式审计入口。**必须用 {@link ObjectProvider} 注入**:{@code audit.enabled=false} 时
|
* 整个自动配置类连同 AuditClient Bean 一起缺席,普通 @Autowired 会让本服务起不来。
|
*/
|
@Autowired
|
private ObjectProvider<AuditClient> auditClientProvider;
|
|
@Override
|
@AuditLog(
|
action = "ACCEPT", label = "样品分检", bizType = "JIANCE_TASK",
|
bizCodeExpr = "#result != null && !#result.isEmpty() ? #param.jianyanLiushuihao : ''",
|
targetTable = "lims_jianyan_xiang", targetIdExpr = "#param.data[0].id",
|
targetIdsExpr = "#param.data.![id]",
|
diff = true, entityClass = LimsJianyanXiangEntity.class
|
)
|
public List<LimsSignUsageTarget> fenjianSamples(LimsJianceTaskAcceptParam param) {
|
if (param == null || param.getData() == null || param.getData().isEmpty()) {
|
return Collections.emptyList();
|
}
|
if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
|
return Collections.emptyList();
|
}
|
|
// 分检人 = 当前登录用户,分检日期 = 当前时间(写入被推进的行)
|
String fenjianRen = UserProvider.getUser().getUserId();
|
Date fenjianRiqi = new Date();
|
// 组合id:整次请求生成一个,仅写入 shifou_zuhe_fenjian=yes 的行(非组合行不会用到)
|
String zuheId = RandomUtil.uuId();
|
// 组合分检量 / 单位:请求级共享值(前端在 param 顶层提交一次),前端传了(非 null)就写入本次分检的检验项行
|
String zuheFenjianLiang = param.getZuheFenjianLiang();
|
String zuheFenjianDanwei = param.getZuheFenjianDanwei();
|
|
// 预加载目标行,读各行样品级 shifou_zuhe_fenjian(入参不带该字段,需查 DB 判断是否组合行)
|
List<String> validIds = param.getData().stream()
|
.filter(i -> i != null && i.getId() != null && !i.getId().isEmpty())
|
.map(LimsFenjianItem::getId)
|
.collect(Collectors.toList());
|
Map<String, LimsJianyanXiangEntity> rowMap = new HashMap<>();
|
if (!validIds.isEmpty()) {
|
for (LimsJianyanXiangEntity r : loadRowsByIds(validIds)) {
|
if (r != null && r.getId() != null) {
|
rowMap.put(r.getId(), r);
|
}
|
}
|
}
|
|
// 必须整批校验完再开始 UPDATE。Controller 会把 DataException 转成 R.error;若边校验边更新,
|
// 后一行非法时前面已更新的行会提交,但 SIGN_USED 尚未生成,形成无签名证据的半批写入。
|
validateFenjianItems(param.getData());
|
List<LimsSignUsageTarget> affectedTargets = new ArrayList<>();
|
|
BigDecimal fenjianShuliang = BigDecimal.ZERO;
|
if (zuheFenjianLiang != null && !zuheFenjianLiang.trim().isEmpty()) {
|
fenjianShuliang = parseFenjianQuantity(zuheFenjianLiang, "组合分检量");
|
}
|
for (LimsFenjianItem item : param.getData()) {
|
if (item == null || item.getId() == null || item.getId().isEmpty()) {
|
continue;
|
}
|
|
LimsJianyanXiangEntity row = rowMap.get(item.getId());
|
if (row == null) {
|
continue;
|
}
|
if (zuheFenjianLiang == null || zuheFenjianLiang.trim().isEmpty()) {
|
fenjianShuliang = fenjianShuliang.add(
|
parseFenjianQuantity(item.getFenjianLiang(), "分检量"));
|
}
|
|
// 仅推进 submit 时创建的 created 行;其余状态 / 已软删 / 不存在 自动跳过(affected=0)
|
UpdateWrapper<LimsJianyanXiangEntity> wrapper = new UpdateWrapper<>();
|
wrapper.eq("f_id", item.getId());
|
wrapper.eq("jianyan_zhuangtai", JianyanStatus.CREATED.getCode());
|
wrapper.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
wrapper.set("jianyan_zhuangtai", JianyanStatus.PENDING_ASSIGN.getCode());
|
wrapper.set("fenjian_zhuangtai", FenjianZhuangtai.FENJIAN.getCode());
|
wrapper.set("fenjian_liang", item.getFenjianLiang());
|
wrapper.set("fenjian_danwei", item.getFenjianDanwei());
|
wrapper.set("fenjian_fenzhuang_rongqi", item.getFenjianFenzhuangRongqi());
|
wrapper.set("fenjian_baocun_tiaojian", item.getFenjianBaocunTiaojian());
|
wrapper.set("fenjian_ren", fenjianRen);
|
wrapper.set("fenjian_riqi", fenjianRiqi);
|
|
// 组合三列仅写该行 DB 上 shifou_zuhe_fenjian=yes 的组合行:
|
// zuhe_id 用本次请求共享的 zuheId;组合分检量 / 单位取请求级共享值,前端传了(非 null)才写、不校验空
|
if ("yes".equalsIgnoreCase(row.getShifouZuheFenjian())) {
|
wrapper.set("zuhe_id", zuheId);
|
if (zuheFenjianLiang != null) {
|
wrapper.set("zuhe_fenjian_liang", zuheFenjianLiang);
|
}
|
if (zuheFenjianDanwei != null) {
|
wrapper.set("zuhe_fenjian_danwei", zuheFenjianDanwei);
|
}
|
}
|
|
int rows = limsJianyanXiangMapper.update(null, wrapper);
|
if (rows > 0) {
|
affectedTargets.add(signUsageTarget(row));
|
}
|
}
|
|
// 更新请验单库存数量
|
updateQingyandanKucun(param.getJianyanLiushuihao(), fenjianShuliang);
|
|
return affectedTargets;
|
}
|
|
@Override
|
@AuditLog(
|
action = "ASSIGN",
|
label = "检验任务分配",
|
bizType = "JIANCE_TASK",
|
bizCodeExpr = "#result[0].bizCode",
|
targetTable = "lims_jianyan_xiang",
|
targetIdExpr = "#param.data[0].id",
|
targetIdsExpr = "#param.data.![id]",
|
diff = true,
|
entityClass = LimsJianyanXiangEntity.class
|
)
|
public List<LimsSignUsageTarget> assignJianyanXiang(LimsJianyanAssignParam param) {
|
if (param == null || param.getData() == null || param.getData().isEmpty()) {
|
return Collections.emptyList();
|
}
|
if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
|
return Collections.emptyList();
|
}
|
|
UserInfo userInfo = UserProvider.getUser();
|
List<LimsSignUsageTarget> affectedTargets = new ArrayList<>();
|
|
// 预加载所有目标行,按 shifou_waijian 决定每行的必填分支:
|
// "yes" → waijian_danwei + waijian_lururen 必填(外检分配)
|
// 其他 → jiancha_ren + wancheng_riqi 必填(普通分配)
|
List<String> validIds = param.getData().stream()
|
.filter(i -> i != null && i.getId() != null && !i.getId().isEmpty())
|
.map(LimsJianyanAssignItem::getId)
|
.collect(Collectors.toList());
|
Map<String, LimsJianyanXiangEntity> rowMap = new HashMap<>();
|
if (!validIds.isEmpty()) {
|
for (LimsJianyanXiangEntity r : loadRowsByIds(validIds)) {
|
if (r != null && r.getId() != null) {
|
rowMap.put(r.getId(), r);
|
}
|
}
|
}
|
validateAssignItems(param.getData(), rowMap);
|
|
for (LimsJianyanAssignItem item : param.getData()) {
|
if (item == null || item.getId() == null || item.getId().isEmpty()) {
|
continue;
|
}
|
LimsJianyanXiangEntity existing = rowMap.get(item.getId());
|
// 没有预加载到的行不执行 UPDATE,保证成功行必然能映射为实际签名目标。
|
if (existing == null) {
|
continue;
|
}
|
boolean isWaijian = "yes".equalsIgnoreCase(existing.getShifouWaijian());
|
|
UpdateWrapper<LimsJianyanXiangEntity> wrapper = new UpdateWrapper<>();
|
Date assignedAt = new Date();
|
wrapper.eq("f_id", item.getId());
|
// 允许初次分配 + 改派:pending_assign / pending_test 均可,审核中 / 已审核禁止
|
wrapper.in("jianyan_zhuangtai",
|
JianyanStatus.PENDING_ASSIGN.getCode(),
|
JianyanStatus.PENDING_TEST.getCode());
|
wrapper.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
wrapper.set("renwu_fenpeiren", userInfo.getUserId());
|
wrapper.set("renwu_fenpei_shijian", assignedAt);
|
wrapper.set("jianyan_zhuangtai", JianyanStatus.PENDING_TEST.getCode());
|
if (item.getHetonghao() != null && !item.getHetonghao().isEmpty()) {
|
wrapper.set("hetonghao", item.getHetonghao());
|
}
|
if (item.getBeizhu() != null && !item.getBeizhu().isEmpty()) {
|
wrapper.set("beizhu", item.getBeizhu());
|
}
|
|
if (isWaijian) {
|
wrapper.set("waijian_danwei", item.getWaijianDanwei());
|
wrapper.set("waijian_lururen", item.getWaijianLururen());
|
// 同时清空检验员 / 完成日期,避免改派切换为外检后残留旧值
|
wrapper.set("jiancha_ren", null);
|
wrapper.set("wancheng_riqi", null);
|
} else {
|
// wancheng_riqi 改为非必填(业务允许"先分配人,后续再补完成日期")。
|
// null / 非 null 都按入参原样写入,由 daily 提醒任务在 IS NOT NULL 时才命中。
|
wrapper.set("jiancha_ren", item.getJianchaRen());
|
wrapper.set("wancheng_riqi", item.getWanchengRiqi());
|
}
|
// 任何分配 / 改派都清空 remind_flags:等同重新计时,下一轮 D-N 重新计算。
|
wrapper.set("remind_flags", null);
|
|
int rows = limsJianyanXiangMapper.update(null, wrapper);
|
if (rows > 0) {
|
affectedTargets.add(signUsageTarget(existing));
|
operationalMessagePublisher.taskAssigned(existing, item, userInfo, assignedAt);
|
}
|
}
|
|
return affectedTargets;
|
}
|
|
@Override
|
@AuditLog(
|
action = "RESULT_SUBMIT", label = "检验结果录入", bizType = "JIANCE_TASK",
|
bizCodeExpr = "#result.affected > 0 ? #result.affectedTargets[0].bizCode : ''",
|
targetTable = "lims_jianyan_xiang", targetIdExpr = "#param.data[0].id",
|
targetIdsExpr = "#param.data.![id]",
|
diff = true, entityClass = LimsJianyanXiangEntity.class
|
)
|
public LimsSubmitResultOutcome submitJianyanResult(LimsJianyanResultParam param) {
|
if (param == null || param.getData() == null || param.getData().isEmpty()) {
|
return new LimsSubmitResultOutcome();
|
}
|
if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
|
return new LimsSubmitResultOutcome();
|
}
|
|
UserInfo userInfo = UserProvider.getUser();
|
String currentUserId = userInfo.getUserId();
|
Date now = new Date();
|
int totalAffected = 0;
|
LimsSubmitResultOutcome outcome = new LimsSubmitResultOutcome();
|
|
List<String> incomingIds = param.getData().stream()
|
.filter(i -> i != null)
|
.map(LimsJianyanResultItem::getId)
|
.collect(Collectors.toList());
|
|
// 预加载行:外检行没有检验员(jiancha_ren=NULL),不能用 jiancha_ren=currentUserId 过滤;
|
// 其他列(qingyandan_id)也会用来后续兜底触发 generateReportData
|
List<String> validIds = incomingIds.stream()
|
.filter(id -> id != null && !id.isEmpty())
|
.collect(Collectors.toList());
|
Map<String, LimsJianyanXiangEntity> rowMap = new HashMap<>();
|
if (!validIds.isEmpty()) {
|
for (LimsJianyanXiangEntity r : loadRowsByIds(validIds)) {
|
if (r != null && r.getId() != null) {
|
rowMap.put(r.getId(), r);
|
}
|
}
|
}
|
Set<String> affectedQingyandanIds = new HashSet<>();
|
List<LimsJianyanXiangEntity> messageRows = new ArrayList<>();
|
|
for (LimsJianyanResultItem item : param.getData()) {
|
if (item == null) {
|
continue;
|
}
|
if (item.getId() == null || item.getId().isEmpty()) {
|
continue;
|
}
|
if (item.getJianyanJieguo() == null || item.getJianyanJieguo().isEmpty()) {
|
continue;
|
}
|
if (item.getJianyanJielun() == null || item.getJianyanJielun().isEmpty()) {
|
continue;
|
}
|
|
LimsJianyanXiangEntity existing = rowMap.get(item.getId());
|
// 没有预加载到的行不执行 UPDATE,避免并发插入后写成功却缺少 bizCode/target 元数据。
|
if (existing == null) {
|
continue;
|
}
|
boolean isWaijian = "yes".equalsIgnoreCase(existing.getShifouWaijian());
|
|
UpdateWrapper<LimsJianyanXiangEntity> wrapper = new UpdateWrapper<>();
|
wrapper.eq("f_id", item.getId());
|
// 允许初次录入(pending_test)或重新录入(audit_rejected 审核退回后)或已撤回的重新录入
|
wrapper.in("jianyan_zhuangtai",
|
JianyanStatus.PENDING_TEST.getCode(),
|
JianyanStatus.CANCELLED.getCode(),
|
JianyanStatus.AUDIT_REJECTED.getCode());
|
// 权限过滤:仅非外检项校验"分配给自己的任务"。外检行 jiancha_ren=NULL,
|
// 不做该校验,由 waijian_lururen / 上下游业务约束兜底。
|
if (!isWaijian) {
|
wrapper.eq("jiancha_ren", currentUserId);
|
}
|
wrapper.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
wrapper.set("jianyan_jieguo", item.getJianyanJieguo());
|
wrapper.set("jianyan_jielun", item.getJianyanJielun());
|
wrapper.set("biz_files", item.getBizFiles());
|
wrapper.set("jieguo_luru_riqi", now);
|
wrapper.set("jianyan_zhuangtai", JianyanStatus.PENDING_AUDIT.getCode());
|
// 写回本行选择的异常调查类型(na/ad/oos/oot),供台账与后续联动使用
|
wrapper.set("yichang_diaocha_leixing", item.getYichangDiaochaLeixing());
|
int rows = limsJianyanXiangMapper.update(null, wrapper);
|
if (rows > 0 && existing.getQingyandanId() != null) {
|
affectedQingyandanIds.add(existing.getQingyandanId());
|
}
|
if (rows > 0) {
|
outcome.getAffectedTargets().add(signUsageTarget(existing));
|
messageRows.add(existing);
|
}
|
// 结果登记成功后,按本行异常调查类型发起 AD/OOS/OOT 调查工作流。
|
// best-effort:失败只记日志 + 收集流水号供响应提示,不上抛(否则会触发 Controller 层事务回滚,连带回滚结果登记)。
|
if (rows > 0) {
|
try {
|
limsYichangDiaochaService.createIfNeeded(existing, item);
|
} catch (Exception e) {
|
log.error("[submit-result] 发起异常调查流程失败 id={} type={}: {}",
|
item.getId(), item.getYichangDiaochaLeixing(), e.getMessage(), e);
|
if (existing.getJianyanLiushuihao() != null && !existing.getJianyanLiushuihao().isEmpty()) {
|
outcome.getFlowLaunchFailed().add(existing.getJianyanLiushuihao());
|
}
|
}
|
}
|
totalAffected += rows;
|
}
|
|
// 兜底触发报告生成。
|
//
|
// 历史背景:早期外检项允许"录入完即就绪",不走复核 —— 当一个 liushuihao
|
// 下全是外检项时,永远不会调到 passJianyanAudit,需要这里在录入后兜底
|
// 尝试生成报告。
|
//
|
// 现状(2026-05-20 起):外检也必须 audited 才视为就绪,见
|
// LimsQingyandanReportServiceImpl.hasUnauditedXiang 的历史变更说明。
|
// 因此本兜底实际等同于 no-op —— 录入完只到 pending_audit,
|
// generateReportData 会被守门拒绝。保留这段调用是"无害的防御网":
|
// 若未来再次出现"无需复核"的快速通道(如紧急放行),不必再加触发点。
|
if (!affectedQingyandanIds.isEmpty()) {
|
for (String qingyandanId : affectedQingyandanIds) {
|
limsQingyandanReportService.generateReportData(qingyandanId);
|
}
|
}
|
|
outcome.setAffected(totalAffected);
|
if (!messageRows.isEmpty()) {
|
operationalMessagePublisher.inspectionResultsPendingReview(messageRows, userInfo, now);
|
}
|
return outcome;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@AuditLog(
|
action = "AUDIT_PASS", label = "检验结果审核通过", bizType = "JIANCE_TASK",
|
bizCodeExpr = "#result.affected > 0 ? #result.affectedTargets[0].bizCode : ''",
|
targetTable = "lims_jianyan_xiang", targetIdExpr = "#param.data[0].id",
|
targetIdsExpr = "#param.data.![id]",
|
diff = true, entityClass = LimsJianyanXiangEntity.class
|
)
|
public LimsJianyanAuditOutcome passJianyanAudit(LimsJianyanAuditParam param) {
|
LimsJianyanAuditOutcome outcome = new LimsJianyanAuditOutcome();
|
if (param == null || param.getData() == null || param.getData().isEmpty()) {
|
return outcome;
|
}
|
outcome.setTotal(param.getData().size());
|
if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
|
return outcome;
|
}
|
List<String> validIds = param.getData().stream()
|
.filter(i -> i != null && i.getId() != null && !i.getId().isEmpty())
|
.map(LimsJianyanAuditItem::getId)
|
.collect(Collectors.toList());
|
if (validIds.isEmpty()) {
|
return outcome;
|
}
|
|
UserInfo auditor = UserProvider.getUser();
|
String auditorId = auditor.getUserId();
|
|
// 一次查回所有未软删的行,缺失即 notFound;行存在但状态不符即 wrongStatus
|
List<LimsJianyanXiangEntity> allRows = loadRowsByIds(validIds);
|
Map<String, LimsJianyanXiangEntity> rowMap = allRows.stream()
|
.filter(r -> r != null && r.getId() != null)
|
.collect(Collectors.toMap(LimsJianyanXiangEntity::getId, r -> r, (a, b) -> a));
|
|
List<LimsJianyanXiangEntity> pendingAuditRows = new ArrayList<>();
|
for (String id : validIds) {
|
LimsJianyanXiangEntity r = rowMap.get(id);
|
if (r == null) {
|
outcome.getNotFoundIds().add(id);
|
} else if (!JianyanStatus.PENDING_AUDIT.getCode().equals(r.getJianyanZhuangtai())) {
|
outcome.getWrongStatusIds().add(id);
|
} else {
|
pendingAuditRows.add(r);
|
}
|
}
|
|
if (pendingAuditRows.isEmpty()) {
|
return outcome;
|
}
|
|
Set<String> auditorOrganizes = fetchAuditorOrganizes(auditorId);
|
List<String> passedIds = classifyAuditableRows(pendingAuditRows, auditorId, auditorOrganizes, outcome);
|
if (passedIds.isEmpty()) {
|
return outcome;
|
}
|
List<String> affectedIds = new ArrayList<>();
|
for (String id : passedIds) {
|
if (markAudited(id, auditorId) > 0) {
|
affectedIds.add(id);
|
outcome.getAffectedTargets().add(signUsageTarget(rowMap.get(id)));
|
}
|
}
|
outcome.setAffected(affectedIds.size());
|
if (!affectedIds.isEmpty()) {
|
tryGenerateReports(affectedIds);
|
}
|
return outcome;
|
}
|
|
/**
|
* 审核通过后,对本批 audited 涉及的请验单逐个尝试生成报告。
|
* 与 audit-pass 在同一事务内:报告生成失败会回滚整个动作。
|
*/
|
private void tryGenerateReports(List<String> auditedIds) {
|
// 不限定 select 列,避免 JNPF 拦截器对部分列结果置 null(见 loadRowsByStatus 注释)。
|
QueryWrapper<LimsJianyanXiangEntity> sq = new QueryWrapper<>();
|
sq.in("f_id", auditedIds);
|
List<LimsJianyanXiangEntity> rows = limsJianyanXiangMapper.selectList(sq);
|
Set<String> qingyandanIds = rows.stream()
|
.filter(r -> r != null)
|
.map(LimsJianyanXiangEntity::getQingyandanId)
|
.filter(s -> s != null && !s.isEmpty())
|
.collect(Collectors.toSet());
|
for (String qingyandanId : qingyandanIds) {
|
limsQingyandanReportService.generateReportData(qingyandanId);
|
}
|
}
|
|
/** 查选中 id 列表中处于指定状态且未软删的行。
|
* 注意:不要用 sq.select(...) 限定列,否则 JNPF 多租户/数据权限拦截器
|
* 在结果映射后置处理阶段会把缺少 f_tenant_id 等系统列的 entity 整个置为 null。 */
|
private List<LimsJianyanXiangEntity> loadRowsByStatus(List<String> ids, JianyanStatus status) {
|
QueryWrapper<LimsJianyanXiangEntity> sq = new QueryWrapper<>();
|
sq.in("f_id", ids);
|
sq.eq("jianyan_zhuangtai", status.getCode());
|
sq.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
return limsJianyanXiangMapper.selectList(sq);
|
}
|
|
/**
|
* 查审核人所属的部门 id 集合(base_user_relation.f_object_type='organize')。
|
* 超级管理员(UserInfo.isAdministrator)返回 null 作为"全班组通行证",
|
* 由 {@link #classifyAuditableRows} 识别后跳过班组校验。
|
*/
|
private Set<String> fetchAuditorOrganizes(String auditorId) {
|
UserInfo userInfo = UserProvider.getUser();
|
if (Boolean.TRUE.equals(userInfo.getIsAdministrator())) {
|
return null;
|
}
|
List<UserRelationEntity> relations = userRelationApi.getList(auditorId, PermissionConst.ORGANIZE);
|
if (relations == null || relations.isEmpty()) {
|
return new HashSet<>();
|
}
|
return relations.stream()
|
.map(UserRelationEntity::getObjectId)
|
.filter(s -> s != null && !s.isEmpty())
|
.collect(Collectors.toSet());
|
}
|
|
/** 逐行写入审核人,使返回值能精确对应真正命中的 ID。 */
|
private int markAudited(String id, String auditorId) {
|
UpdateWrapper<LimsJianyanXiangEntity> uw = new UpdateWrapper<>();
|
uw.eq("f_id", id);
|
uw.eq("jianyan_zhuangtai", JianyanStatus.PENDING_AUDIT.getCode());
|
uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
uw.set("renwu_shenheren", auditorId);
|
uw.set("jianyan_zhuangtai", JianyanStatus.AUDITED.getCode());
|
uw.set("renwu_shenheren_shijian", new Date());
|
return limsJianyanXiangMapper.update(null, uw);
|
}
|
|
@Override
|
public LimsJianyanAuditOutcome failJianyanAudit(LimsJianyanAuditFailParam param) {
|
LimsJianyanAuditOutcome outcome = new LimsJianyanAuditOutcome();
|
if (param == null || param.getData() == null || param.getData().isEmpty()) {
|
return outcome;
|
}
|
outcome.setTotal(param.getData().size());
|
if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
|
return outcome;
|
}
|
if (param.getNote() == null || param.getNote().isEmpty()) {
|
return outcome;
|
}
|
List<String> validIds = param.getData().stream()
|
.filter(i -> i != null && i.getId() != null && !i.getId().isEmpty())
|
.map(LimsJianyanAuditItem::getId)
|
.collect(Collectors.toList());
|
if (validIds.isEmpty()) {
|
return outcome;
|
}
|
|
UserInfo auditor = UserProvider.getUser();
|
String auditorId = auditor.getUserId();
|
|
// 一次查回所有未软删的行,缺失即 notFound;行存在但状态不符即 wrongStatus
|
List<LimsJianyanXiangEntity> allRows = loadRowsByIds(validIds);
|
Map<String, LimsJianyanXiangEntity> rowMap = allRows.stream()
|
.filter(r -> r != null && r.getId() != null)
|
.collect(Collectors.toMap(LimsJianyanXiangEntity::getId, r -> r, (a, b) -> a));
|
|
List<LimsJianyanXiangEntity> pendingAuditRows = new ArrayList<>();
|
for (String id : validIds) {
|
LimsJianyanXiangEntity r = rowMap.get(id);
|
if (r == null) {
|
outcome.getNotFoundIds().add(id);
|
} else if (!JianyanStatus.PENDING_AUDIT.getCode().equals(r.getJianyanZhuangtai())) {
|
outcome.getWrongStatusIds().add(id);
|
} else {
|
pendingAuditRows.add(r);
|
}
|
}
|
|
if (pendingAuditRows.isEmpty()) {
|
return outcome;
|
}
|
|
Set<String> auditorOrganizes = fetchAuditorOrganizes(auditorId);
|
List<String> passedIds = classifyAuditableRows(pendingAuditRows, auditorId, auditorOrganizes, outcome);
|
if (passedIds.isEmpty()) {
|
return outcome;
|
}
|
List<String> affectedIds = new ArrayList<>();
|
for (String id : passedIds) {
|
if (markAuditRejected(id, param.getNote(), auditorId) > 0) {
|
affectedIds.add(id);
|
outcome.getAffectedTargets().add(signUsageTarget(rowMap.get(id)));
|
}
|
}
|
outcome.setAffected(affectedIds.size());
|
|
String auditExtraJson = JsonUtil.getObjectToString(
|
Collections.singletonMap("reason", param.getNote()));
|
for (String id : affectedIds) {
|
LimsJianyanXiangEntity row = rowMap.get(id);
|
if (row == null) continue;
|
recordAuditFailEvent(row.getJianyanLiushuihao(), id, auditExtraJson);
|
}
|
if (!affectedIds.isEmpty()) {
|
List<LimsJianyanXiangEntity> rejectedRows = affectedIds.stream()
|
.map(rowMap::get).filter(java.util.Objects::nonNull).collect(Collectors.toList());
|
operationalMessagePublisher.inspectionResultsRejected(
|
rejectedRows, param.getNote(), auditor, new Date());
|
}
|
return outcome;
|
}
|
|
/**
|
* 层 3 显式写一条「检验结果项目退回」审计事件。
|
*
|
* <p>为什么走层 3 而不是方法级 {@code @AuditLog}:本动作在 {@code for} 循环里**逐项**写 N 条
|
* (每个被退回的检验项一条、bizCode 各不相同),方法级注解只会产出 1 条。
|
*
|
* <p>{@code operationId} **本方法不传**,交 {@code AuditTxHolder} 单源补齐——spec 契约规定
|
* 它由 holder 生成、调用方与切面都不得自行生成。注意「不传」不等于「落库为 NULL」:
|
* {@code submit} 的三条路径(事件自带 → 外层 MDC → 新生成)都不留空,事务分支还会把
|
* 缓冲内所有事件统一覆盖成同一个 {@code buffer.operationId}。controller 上有 @Transactional,
|
* 故本方法在 for 循环里产出的 N 条事件**同属一个操作作用域**,查询侧能折叠成一次操作。
|
*
|
* <p>(勘误:dddce53 原注释写「刻意留空、代价是折叠不到一起」,那是未执行就下的结论;
|
* 2026-07-27 端到端实测 3 条事件共享同一个非空 operation_id,见 tasks/audit-layer3-e2e.sh A6。)
|
*/
|
private void recordAuditFailEvent(String jianyanLiushuihao, String rowId, String extraJson) {
|
auditClientProvider.ifAvailable(client -> client.record(AuditEventDTO.builder()
|
.eventType(AuditConsts.TYPE_BIZ_ACTION)
|
.actionCode(BizLogAction.AUDIT_FAIL.getCode())
|
.actionLabel("检验结果项目退回")
|
.bizType(BizLogType.JIANCE_TASK.getCode())
|
.bizCode(jianyanLiushuihao)
|
.targetTable("lims_jianyan_xiang")
|
.targetId(rowId)
|
.sourceLayer(3)
|
.extra(extraJson)
|
.build()));
|
}
|
|
/** 跟 loadRowsByStatus 同一查询结构但不限定状态,用于做 notFound / wrongStatus 区分。 */
|
private List<LimsJianyanXiangEntity> loadRowsByIds(List<String> ids) {
|
QueryWrapper<LimsJianyanXiangEntity> sq = new QueryWrapper<>();
|
sq.in("f_id", ids);
|
sq.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
return limsJianyanXiangMapper.selectList(sq);
|
}
|
|
/**
|
* 过滤可审核 / 退回的行:审核人不是录入人、且审核人在该行班组内。
|
* auditorOrganizes 为 null 表示超管通行,跳过班组校验(仍保留不能审自己的约束)。
|
* 把"自审自""非本班组"两种失败原因分别记入 outcome,便于 controller 给出针对性错误。
|
*/
|
private List<String> classifyAuditableRows(List<LimsJianyanXiangEntity> rows,
|
String auditorId,
|
Set<String> auditorOrganizes,
|
LimsJianyanAuditOutcome outcome) {
|
List<String> passedIds = new ArrayList<>();
|
for (LimsJianyanXiangEntity r : rows) {
|
if (r == null || r.getId() == null || r.getId().isEmpty()) {
|
continue;
|
}
|
if (auditorId.equals(r.getJianchaRen())) {
|
outcome.getSelfRejectIds().add(r.getId());
|
continue;
|
}
|
if (auditorOrganizes != null) {
|
if (r.getJianchaBanzu() == null || !auditorOrganizes.contains(r.getJianchaBanzu())) {
|
outcome.getNotInBanzuIds().add(r.getId());
|
continue;
|
}
|
}
|
passedIds.add(r.getId());
|
}
|
return passedIds;
|
}
|
|
/** 整批分检入参的纯校验阶段;返回后业务循环才允许执行第一条 UPDATE。 */
|
static void validateFenjianItems(List<LimsFenjianItem> items) {
|
if (items == null) {
|
return;
|
}
|
int idx = 0;
|
for (LimsFenjianItem item : items) {
|
idx++;
|
if (item == null || item.getId() == null || item.getId().isEmpty()) {
|
continue;
|
}
|
String label = "第 " + idx + " 条";
|
if (item.getFenjianLiang() == null || item.getFenjianLiang().isEmpty()) {
|
throw new DataException(label + " 缺少分检量");
|
}
|
if (item.getFenjianDanwei() == null || item.getFenjianDanwei().isEmpty()) {
|
throw new DataException(label + " 缺少分检单位");
|
}
|
if (item.getFenjianFenzhuangRongqi() == null || item.getFenjianFenzhuangRongqi().isEmpty()) {
|
throw new DataException(label + " 缺少分装容器");
|
}
|
if (item.getFenjianBaocunTiaojian() == null || item.getFenjianBaocunTiaojian().isEmpty()) {
|
throw new DataException(label + " 缺少分检后待检样品保存条件");
|
}
|
}
|
}
|
|
private void updateQingyandanKucun(String jianyanLiushuihao, BigDecimal fenjianShuliang) {
|
if (jianyanLiushuihao == null || jianyanLiushuihao.trim().isEmpty()) {
|
throw new DataException("缺少检验流水号");
|
}
|
|
QueryWrapper<LimsQingyandanEntity> queryWrapper = new QueryWrapper<>();
|
queryWrapper.eq("jianyan_liushuihao", jianyanLiushuihao);
|
LimsQingyandanEntity qingyandan = limsQingyandanMapper.selectOne(queryWrapper);
|
if (qingyandan == null) {
|
throw new DataException("未找到检验流水号对应的请验单");
|
}
|
|
BigDecimal kucunShuliang = parseFenjianQuantity(qingyandan.getKucunShuliang(), "库存数量");
|
BigDecimal shengyuShuliang = kucunShuliang.subtract(fenjianShuliang).max(BigDecimal.ZERO);
|
UpdateWrapper<LimsQingyandanEntity> updateWrapper = new UpdateWrapper<>();
|
updateWrapper.eq("f_id", qingyandan.getId());
|
updateWrapper.set("kucun_shuliang", shengyuShuliang.stripTrailingZeros().toPlainString());
|
limsQingyandanMapper.update(null, updateWrapper);
|
}
|
|
private BigDecimal parseFenjianQuantity(String value, String fieldName) {
|
if (value == null || value.trim().isEmpty()) {
|
throw new DataException(fieldName + "不能为空");
|
}
|
try {
|
return new BigDecimal(value.trim());
|
} catch (NumberFormatException e) {
|
throw new DataException(fieldName + "格式不正确");
|
}
|
}
|
|
/** 整批分配入参的纯校验阶段;缺失业务行仍按原有部分成功语义跳过。 */
|
static void validateAssignItems(List<LimsJianyanAssignItem> items,
|
Map<String, LimsJianyanXiangEntity> rowMap) {
|
if (items == null) {
|
return;
|
}
|
int idx = 0;
|
for (LimsJianyanAssignItem item : items) {
|
idx++;
|
if (item == null || item.getId() == null || item.getId().isEmpty()) {
|
continue;
|
}
|
LimsJianyanXiangEntity existing = rowMap == null ? null : rowMap.get(item.getId());
|
if (existing == null) {
|
continue;
|
}
|
String label = existing.getJianyanXiangmuMingcheng() != null
|
? existing.getJianyanXiangmuMingcheng()
|
: "第 " + idx + " 条";
|
if ("yes".equalsIgnoreCase(existing.getShifouWaijian())) {
|
if (item.getWaijianDanwei() == null || item.getWaijianDanwei().isEmpty()) {
|
throw new DataException("外检项「" + label + "」必须填写外检检测单位");
|
}
|
if (item.getWaijianLururen() == null || item.getWaijianLururen().isEmpty()) {
|
throw new DataException("外检项「" + label + "」必须填写外检结果录入人");
|
}
|
} else if (item.getJianchaRen() == null || item.getJianchaRen().isEmpty()) {
|
throw new DataException(label + " 未选择检验员");
|
}
|
}
|
}
|
|
/** 逐行退回,使部分成功时可精确记录实际 ID。 */
|
private int markAuditRejected(String id, String note, String auditorId) {
|
UpdateWrapper<LimsJianyanXiangEntity> uw = new UpdateWrapper<>();
|
uw.eq("f_id", id);
|
uw.eq("jianyan_zhuangtai", JianyanStatus.PENDING_AUDIT.getCode());
|
uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
uw.set("renwu_tuihuiren", auditorId);
|
uw.set("tuihui_yuanyin", note);
|
uw.set("jianyan_zhuangtai", JianyanStatus.AUDIT_REJECTED.getCode());
|
return limsJianyanXiangMapper.update(null, uw);
|
}
|
|
private LimsSignUsageTarget signUsageTarget(LimsJianyanXiangEntity row) {
|
return LimsSignUsageTarget.builder()
|
.targetTable("lims_jianyan_xiang")
|
.targetId(row == null ? null : row.getId())
|
.bizType(BizLogType.JIANCE_TASK.getCode())
|
.bizModule("LIMS-检测任务")
|
.bizCode(row == null ? null : row.getJianyanLiushuihao())
|
.build();
|
}
|
}
|