From 34981c30a78e8bbd7791131059a9210f9928b62c Mon Sep 17 00:00:00 2001
From: 刘光辉 <347230014@qq.com>
Date: 星期四, 17 九月 2026 09:24:09 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master' into master

---
 jnpf-lims/jnpf-lims-biz/src/main/java/jnpf/limsService/impl/LimsJianceTaskServiceImpl.java |  798 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 798 insertions(+), 0 deletions(-)

diff --git a/jnpf-lims/jnpf-lims-biz/src/main/java/jnpf/limsService/impl/LimsJianceTaskServiceImpl.java b/jnpf-lims/jnpf-lims-biz/src/main/java/jnpf/limsService/impl/LimsJianceTaskServiceImpl.java
new file mode 100644
index 0000000..4885c75
--- /dev/null
+++ b/jnpf-lims/jnpf-lims-biz/src/main/java/jnpf/limsService/impl/LimsJianceTaskServiceImpl.java
@@ -0,0 +1,798 @@
+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銆侰ontroller 浼氭妸 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 琛岋紱鍏朵綑鐘舵�� / 宸茶蒋鍒� / 涓嶅瓨鍦� 鑷姩璺宠繃锛坅ffected=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());
+            // 鍏佽鍒濇鍒嗛厤 + 鏀规淳锛歱ending_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());
+
+        // 棰勫姞杞借锛氬妫�琛屾病鏈夋楠屽憳锛坖iancha_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());
+            // 鍏佽鍒濇褰曞叆锛坧ending_test锛夋垨閲嶆柊褰曞叆锛坅udit_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 闆嗗悎锛坆ase_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 鏉�
+     * 锛堟瘡涓閫�鍥炵殑妫�楠岄」涓�鏉°�乥izCode 鍚勪笉鐩稿悓锛夛紝鏂规硶绾ф敞瑙e彧浼氫骇鍑� 1 鏉°��
+     *
+     * <p>{@code operationId} **鏈柟娉曚笉浼�**锛屼氦 {@code AuditTxHolder} 鍗曟簮琛ラ綈鈥斺�攕pec 濂戠害瑙勫畾
+     * 瀹冪敱 holder 鐢熸垚銆佽皟鐢ㄦ柟涓庡垏闈㈤兘涓嶅緱鑷鐢熸垚銆傛敞鎰忋�屼笉浼犮�嶄笉绛変簬銆岃惤搴撲负 NULL銆嶏細
+     * {@code submit} 鐨勪笁鏉¤矾寰勶紙浜嬩欢鑷甫 鈫� 澶栧眰 MDC 鈫� 鏂扮敓鎴愶級閮戒笉鐣欑┖锛屼簨鍔″垎鏀繕浼氭妸
+     * 缂撳啿鍐呮墍鏈変簨浠剁粺涓�瑕嗙洊鎴愬悓涓�涓� {@code buffer.operationId}銆俢ontroller 涓婃湁 @Transactional锛�
+     * 鏁呮湰鏂规硶鍦� for 寰幆閲屼骇鍑虹殑 N 鏉′簨浠�**鍚屽睘涓�涓搷浣滀綔鐢ㄥ煙**锛屾煡璇晶鑳芥姌鍙犳垚涓�娆℃搷浣溿��
+     *
+     * <p>锛堝嫎璇細dddce53 鍘熸敞閲婂啓銆屽埢鎰忕暀绌恒�佷唬浠锋槸鎶樺彔涓嶅埌涓�璧枫�嶏紝閭f槸鏈墽琛屽氨涓嬬殑缁撹锛�
+     * 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();
+    }
+}

--
Gitblit v1.8.0