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": "<lims_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<String> 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<String> fenjianIds = param.getData().stream().map(d -> d.getId()).collect(Collectors.toList());
|
|
List<LimsSignUsageTarget> 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。
|
*
|
* <p>校验下沉到 service:行的 shifou_waijian 由 service 预查后决定分支必填字段,
|
* 不在 controller 层根据入参直接判断(避免把"是否外检"由前端决定)。
|
*/
|
@Transactional(rollbackFor = Exception.class)
|
@Operation(summary = "检验任务逐行分配")
|
@PostMapping("/assign")
|
public R<String> 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<String> assignIds = param.getData().stream().map(d -> d.getId()).collect(Collectors.toList());
|
|
List<LimsSignUsageTarget> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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": "<lims_jianyan_xiang.f_id>", "biz_sign": "<lims_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<String> cancelResult(@RequestBody Map<String, String> 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<String> 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);
|
}
|
|
/**
|
* 手动触发"要求完成日期"提醒扫描(开发 / 紧急补发场景)。
|
*
|
* <p>正常生产由 cron 每天 8:00 自动触发;本端点为绕过 cron 的运维入口。
|
*
|
* @param dry true 仅日志不发消息、不写 remind_flags(开发期调试默认)
|
* @return 各分支统计 {hit, sent, skipped}
|
*/
|
@Operation(summary = "手动触发完成日期提醒扫描")
|
@PostMapping("/remind/run-now")
|
public R<Map<String, Map<String, Integer>>> remindRunNow(
|
@RequestParam(value = "dry", defaultValue = "true") boolean dry) {
|
Map<String, Map<String, Integer>> stats = limsJianceTaskRemindService.scanAndRemind(dry);
|
return R.success(stats);
|
}
|
}
|