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-controller/src/main/java/jnpf/limsController/LimsJianceTaskController.java |  454 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 454 insertions(+), 0 deletions(-)

diff --git a/jnpf-lims/jnpf-lims-controller/src/main/java/jnpf/limsController/LimsJianceTaskController.java b/jnpf-lims/jnpf-lims-controller/src/main/java/jnpf/limsController/LimsJianceTaskController.java
new file mode 100644
index 0000000..b10f76a
--- /dev/null
+++ b/jnpf-lims/jnpf-lims-controller/src/main/java/jnpf/limsController/LimsJianceTaskController.java
@@ -0,0 +1,454 @@
+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 鏃跺垱寤虹殑妫�楠岄」锛坈reated + 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锛歭ims_sign 涓婚敭锛宱p_type=jiance_task_accept锛涚鍚嶄粎浣滃璁¤褰曞湪 lims_sign锛屼笟鍔¤〃涓嶈惤 sign 鍒椼��
+     * 缁勫悎涓夊垪锛坺uhe_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锛屼笟鍔¤〃涓嶈惤绛惧悕鍒楋級锛沶ote 鍏变韩 鈫� 姣忚 tuihui_yuanyin锛�
+     * 閫�鍥炰汉锛坮enwu_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 鍚勫垎妗跺ぇ灏忕粰鍑洪拡瀵规�у師鍥犮��
+     * 鍗曚竴鍘熷洜鏃剁洿鎺ヨ鏄庯紝澶氬師鍥犲苟瀛樻椂鍒嗙被鍒楀嚭銆俛udit-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锛宱p_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>姝e父鐢熶骇鐢� cron 姣忓ぉ 8:00 鑷姩瑙﹀彂锛涙湰绔偣涓虹粫杩� cron 鐨勮繍缁村叆鍙c��
+     *
+     * @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);
+    }
+}

--
Gitblit v1.8.0