From 6356db280804feed69ac8c31cce50ec4dfc5fd9c Mon Sep 17 00:00:00 2001
From: yxh <172933527@qq.com>
Date: 星期二, 13 十月 2026 09:21:41 +0800
Subject: [PATCH] 修改了报销校验

---
 ruoyi-project/src/main/java/com/ruoyi/project/service/ExpertFeeCheckResult.java          |  121 +++++
 ruoyi-project/src/main/java/com/ruoyi/project/common/FundNotifyMessageBuilder.java       |  180 +++++++
 ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ExpertFeeValidateService.java | 1078 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1,379 insertions(+), 0 deletions(-)

diff --git a/ruoyi-project/src/main/java/com/ruoyi/project/common/FundNotifyMessageBuilder.java b/ruoyi-project/src/main/java/com/ruoyi/project/common/FundNotifyMessageBuilder.java
new file mode 100644
index 0000000..e09eb52
--- /dev/null
+++ b/ruoyi-project/src/main/java/com/ruoyi/project/common/FundNotifyMessageBuilder.java
@@ -0,0 +1,180 @@
+package com.ruoyi.project.common;
+
+import com.ruoyi.common.enums.ApplyTypeEnum;
+import com.ruoyi.common.utils.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 鎶ラ攢瀹℃壒閽夐拤娑堟伅鍐呭缁勮銆�
+ *
+ * <p>缁熶竴銆屾爣棰� + 鍗曟嵁淇℃伅 + 瀹℃壒鎰忚鏂囨銆嶏紝渚涗互涓嬪彂閫佺偣澶嶇敤锛�
+ * <ul>
+ *   <li>鑷磋繙 OA 鍥炰紶瀹℃壒鎰忚 鈥斺�� {@code ServiceFundServiceImpl#processlog} 鈫� {@code sendMeg}</li>
+ *   <li>绯荤粺鍐呭鏍搁┏鍥烇紙璐圭敤鎶ラ攢锛� 鈥斺�� {@code ServiceFundServiceImpl#checkFund}</li>
+ *   <li>绯荤粺鍐呭鏍搁┏鍥烇紙宸梾璐癸級 鈥斺�� {@code ServiceReimbursementServiceImpl#checkFund}</li>
+ *   <li>宸梾璐逛笂鎶ユ彁浜� 鈥斺�� {@code ServiceReimbursementController#travelexpensereport}</li>
+ * </ul>
+ *
+ * <p><b>涓轰粈涔堣涓�琛屼竴涓� map锛�</b>{@code DingTalkReqVo.contents} 鐨勫厓绱犳槸
+ * {@link ConcurrentHashMap}锛屽叾閬嶅巻椤哄簭涓嶄繚璇侊紱涓�鏉� map 閲屽澶氳浼氬鑷撮拤閽夎〃鍗曡搴忛敊涔便��
+ * 杩欓噷姣忔潯淇℃伅鍗曠嫭涓�涓�屽崟閿� map銆嶏紝鐢� List 鐨勯『搴忎繚璇佹樉绀洪『搴忋��
+ *
+ * @author ruoyi
+ */
+public final class FundNotifyMessageBuilder {
+
+    /** 閽夐拤娑堟伅缁熶竴鏍囬锛�2026-10-08 涓氬姟鏂圭‘璁ょ粺涓�涓恒�孫PO鎶ラ攢瀹℃壒銆嶏級 */
+    public static final String TITLE = "OPO鎶ラ攢瀹℃壒";
+
+    /** 瀹℃壒缁撹锛氶�氳繃 */
+    public static final String CONCLUSION_PASS = "1";
+    /** 瀹℃壒缁撹锛氶┏鍥� */
+    public static final String CONCLUSION_REJECT = "2";
+    /** 瀹℃壒缁撹锛氬畬缁� */
+    public static final String CONCLUSION_FINISH = "3";
+
+    private static final String KEY_TYPE = "鎶ラ攢鍗曠被鍨嬶細";
+    private static final String KEY_BENEFICIARY = "鎶ラ攢浜猴細";
+    private static final String KEY_AMOUNT = "閲戦锛�";
+    private static final String KEY_DONOR = "鎹愮尞妗堜緥锛�";
+    private static final String KEY_BILL_NO = "鍗曟嵁缂栧彿锛�";
+    private static final String KEY_TRAVELERS = "鍑哄樊浜猴細";
+    private static final String KEY_REASON = "鍑哄樊浜嬬敱锛�";
+    private static final String KEY_CHECKER = "瀹℃壒浜猴細";
+    private static final String KEY_CHECK_TIME = "瀹℃壒鏃堕棿锛�";
+    private static final String KEY_OPINION = "鎰忚锛�";
+    private static final String KEY_SUBMITTER = "鎻愪氦浜猴細";
+    private static final String KEY_SUBMIT_TIME = "鎻愪氦鏃堕棿锛�";
+    private static final String KEY_CONTENT = "鍐呭锛�";
+
+    private FundNotifyMessageBuilder() {
+    }
+
+    /** 鏂板缓鍐呭琛屽鍣� */
+    public static List<ConcurrentHashMap<String, Object>> newRows() {
+        return new ArrayList<ConcurrentHashMap<String, Object>>();
+    }
+
+    /**
+     * 杩藉姞涓�琛屻�傚�间负绌猴紙null / 绌轰覆 / 鍏ㄧ┖鐧斤級鏃�**涓嶈拷鍔�**锛岄伩鍏嶉拤閽夐噷鍑虹幇绌鸿銆�
+     */
+    public static void addRow(List<ConcurrentHashMap<String, Object>> rows, String key, Object value) {
+        if (rows == null || key == null || value == null) {
+            return;
+        }
+        if (value instanceof String && StringUtils.isEmpty(((String) value).trim())) {
+            return;
+        }
+        ConcurrentHashMap<String, Object> row = new ConcurrentHashMap<String, Object>(1);
+        row.put(key, value);
+        rows.add(row);
+    }
+
+    /** 鎶ラ攢鍗曠被鍨嬶細applytype 鈫� 涓枃鍚嶏紱鏈尮閰嶈繑鍥� null锛堣琛屼笉鏄剧ず锛� */
+    public static String typeName(String applytype) {
+        return StringUtils.isEmpty(applytype) ? null : ApplyTypeEnum.getDescByCode(applytype);
+    }
+
+    /** 鍗曟嵁缂栧彿锛氫紭鍏堝彇涓氬姟缂栧彿 bh锛岀己澶辨椂閫�鍥炰富閿� id */
+    public static String billNo(String bh, Long id) {
+        if (StringUtils.isNotEmpty(bh)) {
+            return bh;
+        }
+        return id == null ? null : String.valueOf(id);
+    }
+
+    /**
+     * 杩藉姞鎶ラ攢鍗曞熀纭�淇℃伅锛氭姤閿�鍗曠被鍨� / 鎶ラ攢浜� / 閲戦 / 鎹愮尞妗堜緥 / 鍗曟嵁缂栧彿銆�
+     *
+     * <p>閲戦鍙e緞锛氳垂鐢ㄧ被鍙� {@code service_fund.pretaxcost}锛堝鏍搁〉銆岀敵璇烽噾棰濄�嶅垪锛夛紝
+     * 宸梾璐瑰彇 {@code service_reimbursement.amountrequested}锛堝鏍搁〉銆屾姤閿�閲戦銆嶅垪锛夈��
+     */
+    public static void addBillInfo(List<ConcurrentHashMap<String, Object>> rows,
+                                   String applytype, String beneficiary, Double amount,
+                                   String donorname, String billNo) {
+        addRow(rows, KEY_TYPE, typeName(applytype));
+        addRow(rows, KEY_BENEFICIARY, beneficiary);
+        addRow(rows, KEY_AMOUNT, money(amount));
+        addRow(rows, KEY_DONOR, donorname);
+        addRow(rows, KEY_BILL_NO, billNo);
+    }
+
+    /** 杩藉姞宸梾璐逛笓鏈変俊鎭細鍑哄樊浜� / 鍑哄樊浜嬬敱 */
+    public static void addTravelInfo(List<ConcurrentHashMap<String, Object>> rows,
+                                     String travelers, String reason) {
+        addRow(rows, KEY_TRAVELERS, travelers);
+        addRow(rows, KEY_REASON, reason);
+    }
+
+    /**
+     * 杩藉姞瀹℃壒缁撴灉锛氬鎵逛汉 / 瀹℃壒鏃堕棿 / 鎰忚銆�
+     *
+     * <p>瀹℃壒鏃堕棿浼� {@link Date}锛氶敭鍚嶅惈銆屽鎵规椂闂淬�嶏紝鐢� {@code DingTalkServiceImpl} 缁熶竴鏍煎紡鍖栥��
+     */
+    public static void addAuditResult(List<ConcurrentHashMap<String, Object>> rows,
+                                     String checker, Date checkTime, String opinion) {
+        addRow(rows, KEY_CHECKER, checker);
+        addRow(rows, KEY_CHECK_TIME, checkTime);
+        addRow(rows, KEY_OPINION, opinion);
+    }
+
+    /** 杩藉姞鎻愪氦閫氱煡淇℃伅锛氭彁浜や汉 / 鎻愪氦鏃堕棿 / 鍐呭锛堟椂闂村湪姝ら鏍煎紡鍖栵紝閬垮厤 Date.toString()锛� */
+    public static void addSubmitInfo(List<ConcurrentHashMap<String, Object>> rows,
+                                    String submitter, Date submitTime, String content) {
+        addRow(rows, KEY_SUBMITTER, submitter);
+        addRow(rows, KEY_SUBMIT_TIME, time(submitTime));
+        addRow(rows, KEY_CONTENT, content);
+    }
+
+    /**
+     * 瀹℃壒鎰忚鏂囨锛�2026-10-08 涓氬姟鏂圭‘璁わ級銆�
+     *
+     * <pre>
+     *   閫氳繃(1)  鈫� "瀹℃壒閫氳繃銆�"   + 鎰忚澶囨敞锛堣嫢鏈夛級
+     *   椹冲洖(2)  鈫� "瀹℃壒鏈�氳繃銆�" + 鎰忚澶囨敞锛堣嫢鏈夛級
+     *   瀹岀粨(3)  鈫� "娴佺▼宸插畬缁撱��"
+     *   鍏朵粬/绌�  鈫� 鍘熸牱杩斿洖澶囨敞锛涙棤澶囨敞杩斿洖绌轰覆
+     * </pre>
+     *
+     * <p>瀹炴祴渚濇嵁锛堟湰鍦板簱 {@code service_fundflow.flowlevel=100} 鍗宠嚧杩滃洖浼犺褰曪級锛�
+     * 閫氳繃 11618 鏉′腑 9606 鏉℃剰瑙佷负绌恒�佸畬缁� 1457 鏉″叏閮ㄤ负绌恒�侀┏鍥� 743 鏉′腑 161 鏉′负绌恒��
+     * 鑻ヤ笉缁欑粨璁哄厹搴曟枃妗堬紝閽夐拤娑堟伅鐨勩�屾剰瑙併�嶄竴鏍忓氨鏄┖鐧斤紙杩欐鏄笟鍔″弽棣堢殑闂锛夈��
+     */
+    public static String opinion(String conclusion, String content) {
+        String remark = content == null ? "" : content.trim();
+        String base;
+        if (CONCLUSION_PASS.equals(conclusion)) {
+            base = "瀹℃壒閫氳繃銆�";
+        } else if (CONCLUSION_REJECT.equals(conclusion)) {
+            base = "瀹℃壒鏈�氳繃銆�";
+        } else if (CONCLUSION_FINISH.equals(conclusion)) {
+            base = "娴佺▼宸插畬缁撱��";
+        } else {
+            // 缁撹缂哄け锛氬彧鏄剧ず澶囨敞锛屼笉鑷嗘祴缁撹
+            return remark;
+        }
+        return remark.isEmpty() ? base : base + remark;
+    }
+
+    /** 閲戦鏄剧ず锛氫袱浣嶅皬鏁般�佹棤鍗冨垎浣嶏紝濡� {@code 1200.00 鍏儅 */
+    public static String money(Double amount) {
+        if (amount == null) {
+            return null;
+        }
+        return new BigDecimal(String.valueOf(amount))
+                .setScale(2, RoundingMode.HALF_UP)
+                .toPlainString() + " 鍏�";
+    }
+
+    /** 鏃堕棿鏄剧ず锛歿@code yyyy-MM-dd HH:mm:ss}锛�24 灏忔椂鍒讹級 */
+    public static String time(Date date) {
+        return date == null ? null : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
+    }
+}
diff --git a/ruoyi-project/src/main/java/com/ruoyi/project/service/ExpertFeeCheckResult.java b/ruoyi-project/src/main/java/com/ruoyi/project/service/ExpertFeeCheckResult.java
new file mode 100644
index 0000000..c5a3de5
--- /dev/null
+++ b/ruoyi-project/src/main/java/com/ruoyi/project/service/ExpertFeeCheckResult.java
@@ -0,0 +1,121 @@
+package com.ruoyi.project.service;
+
+import com.ruoyi.project.domain.ServiceFunddetailVO;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 涓撳鍔冲姟璐规牎楠岀粨鏋�
+ *
+ * 鍖哄垎涓ょ被缁撴灉锛�
+ * - errors   闃绘柇鎬ч棶棰橈細缁忓姙浜轰笉鑳戒繚瀛樸�佸鎵逛汉涓嶈兘瀹℃壒閫氳繃
+ * - warnings 鎻愮ず鎬ч棶棰橈細涓嶉樆鏂祦绋嬶紝浠呮彁绀虹粡鍔炰汉鑷澶勭悊锛堣鍒� 5銆�6锛�
+ *
+ * @author ruoyi
+ */
+@Data
+public class ExpertFeeCheckResult {
+
+    /** 闃绘柇鎬ч敊璇紙瓒呴檺棰濈瓑锛� */
+    private List<String> errors = new ArrayList<>();
+
+    /** 鎻愮ず淇℃伅锛堝悓涓�涓撳璺ㄩ」鐩彇楂樸�佺浜屽櫒瀹樿秴 500 绛夛級 */
+    private List<String> warnings = new ArrayList<>();
+
+    public void addError(String message) {
+        this.errors.add(message);
+    }
+
+    public void addWarning(String message) {
+        this.warnings.add(message);
+    }
+
+    /** 鏄惁瀛樺湪闃绘柇鎬ч棶棰� */
+    public boolean hasError() {
+        return !this.errors.isEmpty();
+    }
+
+    public boolean hasWarning() {
+        return !this.warnings.isEmpty();
+    }
+
+    /** 閿欒淇℃伅鎷间负涓�琛岋紝渚夸簬鎺ュ彛鐩存帴杩斿洖 */
+    public String errorMessage() {
+        return String.join("锛�", this.errors);
+    }
+
+    /** 鎻愮ず淇℃伅鎷间负涓�琛� */
+    public String warningMessage() {
+        return String.join("锛�", this.warnings);
+    }
+
+    /** 閿欒 + 鎻愮ず鍚堝苟锛堢敤浜庡墠绔竴娆℃�у睍绀猴級 */
+    public String allMessage() {
+        List<String> all = new ArrayList<>();
+        all.addAll(this.errors);
+        all.addAll(this.warnings);
+        return String.join("锛�", all);
+    }
+
+    /**
+     * 鏍¢獙鐢ㄧ殑鏄庣粏琛岋紙鎶婇噾棰濈粺涓�鎴� BigDecimal锛岄伩鍏嶅悗缁� double 杩愮畻锛�
+     */
+    @Data
+    public static class DetailRow {
+        /** 鏄庣粏涓婚敭锛堣惤搴撳悗鎵嶆湁鍊硷級 */
+        private Long id;
+        private Long fundid;
+        private Long infoid;
+        private Long itemid;
+        private String itemname;
+        private String idcardno;
+        private String servicesscopename;
+        /** 绋庡墠閲戦 */
+        private java.math.BigDecimal amount;
+        /** 绋庡悗閲戦 */
+        private java.math.BigDecimal taxedamount;
+
+        public static DetailRow of(ServiceFunddetailVO vo) {
+            DetailRow row = new DetailRow();
+            row.setId(vo.getId());
+            row.setFundid(vo.getFundid());
+            row.setInfoid(vo.getInfoid());
+            row.setItemid(vo.getItemid());
+            row.setItemname(vo.getItemname());
+            row.setIdcardno(vo.getIdcardno());
+            row.setServicesscopename(vo.getServicesscopename());
+            row.setAmount(toDecimal(vo.getAmount()));
+            row.setTaxedamount(toDecimal(vo.getTaxedamount()));
+            return row;
+        }
+
+        private static java.math.BigDecimal toDecimal(Double value) {
+            return value == null ? java.math.BigDecimal.ZERO : java.math.BigDecimal.valueOf(value);
+        }
+
+        /** 璐圭敤璇存槑鏄惁鍖呭惈鍏抽敭瀛楋紙璐圭敤璇存槑鍙兘涓虹┖锛� */
+        public boolean scopeContains(String keyword) {
+            return this.servicesscopename != null && this.servicesscopename.contains(keyword);
+        }
+
+        /**
+         * 璐圭敤璇存槑鏄惁鍖呭惈浠讳竴鍏抽敭瀛楋紙璐圭敤璇存槑鍙兘涓虹┖锛夈��
+         *
+         * <p>鍚屼竴鍦烘櫙鍦ㄤ笉鍚岀幆澧�/骞村害鐨勪笅鎷夋枃鏈笉涓�鑷达紙濡傘�屽競鍐呫�嶄笌銆屾湰甯傘�嶏級锛�
+         * 鐢ㄥ叧閿瓧闆嗗悎鍏煎锛岄伩鍏嶅洜鏂囨宸紓瀵艰嚧闄愰鏍¢獙闈欓粯澶辨晥銆�
+         */
+        public boolean scopeContainsAny(String... keywords) {
+            if (this.servicesscopename == null || keywords == null) {
+                return false;
+            }
+            for (String keyword : keywords) {
+                if (keyword != null && this.servicesscopename.contains(keyword)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ExpertFeeValidateService.java b/ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ExpertFeeValidateService.java
new file mode 100644
index 0000000..6d297f4
--- /dev/null
+++ b/ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ExpertFeeValidateService.java
@@ -0,0 +1,1078 @@
+package com.ruoyi.project.service.impl;
+
+import com.ruoyi.project.domain.BaseFinancesubject;
+import com.ruoyi.project.domain.BaseItemexpense;
+import com.ruoyi.project.domain.ServiceFund;
+import com.ruoyi.project.domain.ServiceFunddetail;
+import com.ruoyi.project.domain.ServiceFunddetailVO;
+import com.ruoyi.project.domain.vo.ServiceFundVO;
+import com.ruoyi.project.mapper.BaseFinancesubjectMapper;
+import com.ruoyi.project.mapper.BaseItemexpenseMapper;
+import com.ruoyi.project.mapper.ServiceFundMapper;
+import com.ruoyi.project.mapper.ServiceFunddetailMapper;
+import com.ruoyi.project.service.ExpertFeeCheckResult;
+import com.ruoyi.project.service.ExpertFeeCheckResult.DetailRow;
+import com.ruoyi.system.service.ISysConfigService;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * 涓撳鍔冲姟璐规牎楠屾湇鍔★紙9 鏉¤鍒欙級
+ *
+ * <p>涓氬姟鑳屾櫙锛氬悓涓�鎹愮尞妗堜緥锛坰ervice_fund.infoid锛夊彲鑳芥湁澶氭潯涓撳鍔冲姟璐规姤閿�鍗�
+ * 锛坰ervice_fund.applytype = '1'锛夛紝鎵�鏈夎垂鐢ㄦ帶鍒堕兘鏄拡瀵瑰悓涓�妗堜緥杩涜鐨勭疮璁℃帶鍒躲��
+ *
+ * <p>鏍¢獙鏃舵満锛堜袱澶勯兘璋冪敤锛夛細
+ * <ul>
+ *   <li>鎶ラ攢鍗曞~鎶ヤ繚瀛樺墠 鈥斺�� ServiceFundServiceImpl#addOrUpdateNew</li>
+ *   <li>鎶ラ攢鍗曞鏍搁�氳繃鍓� 鈥斺�� ServiceFundServiceImpl#checkFund</li>
+ * </ul>
+ *
+ * <p>瑙勫垯鏄庣粏锛�
+ * <ol>
+ *   <li>鎹愮尞鍖婚櫌鍣ㄥ畼鍔熻兘缁存姢璐�(itemid=5) 鍚堣 鈮� 60000锛堢◣鍓嶏級</li>
+ *   <li>涓婄骇鎹愮尞鍖婚櫌鍖荤枟鍥㈤槦鍣ㄥ畼鍔熻兘缁存姢璐�(itemid=7) 鍚堣 鈮� 11000锛堢◣鍚庯級</li>
+ *   <li>涓婄骇鎹愮尞鍖婚櫌鎶ょ悊鍥㈤槦鍣ㄥ畼鍔熻兘缁存姢璐�(itemid=8) 鍚堣 鈮� 1000锛堢◣鍚庯級</li>
+ *   <li>ECOM鍥㈤槦鍔冲姟璐�(itemid=2)锛氶櫌鍐� 鈮� 5000 / 甯傚唴 鈮� 10000 / 甯傚 鈮� 12000锛堟寜璐圭敤璇存槑鍒嗘。锛�</li>
+ *   <li>鍚屼竴涓撳涓嶅緱鍚屾椂棰嗗彇銆屾崘鐚尰闄㈠垵姝ヨ瘎浼拌垂鐢�(1)銆嶃�屾崘鐚梾鎯呰瘎浼颁笓瀹惰垂(18)銆�
+ *       銆岃剳姝讳骸涓村簥鍒ゅ畾涓撳璐�(19)銆嶏紝鍙兘灏遍珮鍙栧叾涓� 鈥斺�� 浠呮彁绀猴紝涓嶉樆鏂�</li>
+ *   <li><b>鍩烘湰瑙勫垯锛�2026-10-08 鏂板锛�</b>锛宎pplytype=1 鐨�<b>鍏ㄩ儴鏄庣粏</b>锛�
+ *       鍚屼竴妗堜緥 脳 鍚屼竴涓撳 脳 鍚屼竴椤圭洰 脳 鍚屼竴璐圭敤璇存槑鐨�<b>鍚堣</b>涓嶅緱瓒呰繃
+ *       base_itemexpense锛坉el_flag=0 涓� UseState=1锛変腑璇ラ」鐩殑璐圭敤鏍囧噯锛�
+ *       閲戦涓� 0銆佽垂鐢ㄨ鏄庝负绌恒�佸尮閰嶄笉鍒版爣鍑嗭紝涓�寰嬮樆鏂紱鍚岀粍鍑虹幇澶氭潯鍙彁绀轰笉闃绘柇</li>
+ *   <li>鍣ㄥ畼鑾峰彇鍔犳垚锛�2026-10-08 閲嶅啓锛宨temid 32~39锛夛細鍚屼竴妗堜緥鍚屼竴涓撳鍦ㄨ绫婚」鐩笂
+ *       鑾峰彇澶氫釜鍣ㄥ畼鏃讹紝鎸夐噾棰濋檷搴忎繚楂橈紝绗簩涓強浠ュ悗鐨勬瘡鏉¢噾棰濅笂闄� =
+ *       璐圭敤璇存槑鍚�屽弻渚с�嶅彇 1000銆佸叾浠栧彇 500锛堥樆鏂紝鍙傛暟瑙� sys_config锛�</li>
+ *   <li>鍔╂墜璐圭敤锛氭寜鍔╂墜绫诲瀷锛坕temid 41銆�71銆�72銆�73銆�74锛�<b>鍒嗗埆</b>鍚堣锛屽競鍐� 鈮� 1000 / 甯傚 鈮� 2000</li>
+ *   <li>鎵嬫湳瀹や汉宸ヨ垂-宸″洖(itemid=43 涓旇垂鐢ㄨ鏄庡惈鈥滃贰鍥炩��) 鈮� 500</li>
+ *   <li>鎵嬫湳瀹や汉宸ヨ垂-楹婚唹(itemid=43 涓旇垂鐢ㄨ鏄庡惈鈥滈夯閱夆��) 鈮� 500</li>
+ * </ol>
+ *
+ * <p>閲戦涓�寰嬩娇鐢� BigDecimal 绱姞锛岀姝� double 杩愮畻锛堟湰椤圭洰姝ゅ墠鍑虹幇杩囬噾棰濇诞鐐圭簿搴﹂棶棰橈級銆�
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@Service
+public class ExpertFeeValidateService {
+
+    /** 涓撳鍔冲姟璐圭殑 applytype 鍙栧�� */
+    private static final String APPLYTYPE_EXPERT = "1";
+
+    /** 璐圭敤璇存槑涓殑鍦烘櫙鍏抽敭瀛楋紙瀹㈡埛鐜瀹為檯涓嬫媺鏂囨湰涓恒�岄櫌鍐呴櫌鍖� / 甯傚唴 / 甯傚銆嶏紝鍚屾椂鍏煎銆屾湰甯� / 澶栧競銆嶆棫鍐欐硶锛� */
+    private static final String[] SCOPE_IN_HOSPITAL = {"闄㈠唴"};
+    private static final String[] SCOPE_IN_CITY = {"甯傚唴", "鏈競"};
+    private static final String[] SCOPE_OUT_CITY = {"甯傚", "澶栧競"};
+    private static final String SCOPE_TAXED = "绋庡悗";
+    private static final String SCOPE_CIRCUIT = "宸″洖";
+    private static final String SCOPE_ANESTHESIA = "楹婚唹";
+    /** 璐圭敤璇存槑涓殑銆屽弻渚с�嶅叧閿瓧锛堣偩銆佽偤鐨勮幏鍙栧尯鍒嗗崟渚�/鍙屼晶锛� */
+    private static final String SCOPE_DOUBLE_SIDE = "鍙屼晶";
+
+    // ---- 璐圭敤椤圭洰锛氬叏閮ㄦ寜椤圭洰 id 鍒ゅ畾 ----
+    // 瑙勫垯涓嶅啀渚濊禆椤圭洰鍚嶇О锛堝悕绉版瘡骞村甫骞村害鍓嶇紑銆佸悇闄篃鍙兘涓嶅悓锛夛紝
+    // 缁熶竴鐢� sys_config 鐨� sys.expertfee.itemid.* 閰嶇疆椤圭洰 id锛堥�楀彿鍒嗛殧锛屽彲澶氫釜锛夈��
+
+    // ---- 鍏煎鍘嗗彶骞村害鐨勯」鐩� id锛堥」鐩〃鏀归�犲墠浣跨敤锛� ----
+    // 涓庨厤缃」姹傚苟闆嗭紝淇濊瘉鍘嗗彶鍗曟嵁浠嶈兘琚瘑鍒紱閰嶇疆椤规槸涓昏渚濇嵁銆�
+    private static final Set<Long> FALLBACK_ORGAN_MAINTAIN = setOf(5L);
+    private static final Set<Long> FALLBACK_SUPERIOR_MEDICAL = setOf(7L);
+    private static final Set<Long> FALLBACK_SUPERIOR_NURSING = setOf(8L);
+    private static final Set<Long> FALLBACK_ECOM = setOf(2L);
+    private static final Set<Long> FALLBACK_OPERATION_ROOM = setOf(43L);
+    private static final Set<Long> FALLBACK_ASSESS = setOf(1L, 18L, 19L);
+    private static final Set<Long> FALLBACK_DOCTOR_ORGAN = setOf(32L, 33L, 34L, 35L, 36L, 37L, 38L, 39L);
+    private static final Set<Long> FALLBACK_ASSISTANT = setOf(41L, 71L, 72L, 73L, 74L);
+
+    // ---- sys_config 閰嶇疆閿� ----
+    private static final String CFG_ORGAN_MAINTAIN = "sys.expertfee.organMaintain.limit";
+    private static final String CFG_SUPERIOR_MEDICAL = "sys.expertfee.superiorMedical.limit";
+    private static final String CFG_SUPERIOR_NURSING = "sys.expertfee.superiorNursing.limit";
+    private static final String CFG_ECOM_IN_HOSPITAL = "sys.expertfee.ecom.limit.inhospital";
+    private static final String CFG_ECOM_IN_CITY = "sys.expertfee.ecom.limit.incity";
+    private static final String CFG_ECOM_OUT_CITY = "sys.expertfee.ecom.limit.outcity";
+    private static final String CFG_ASSISTANT_IN_CITY = "sys.expertfee.assistant.limit.incity";
+    private static final String CFG_ASSISTANT_OUT_CITY = "sys.expertfee.assistant.limit.outcity";
+    /** 鎵嬫湳瀹や汉宸ヨ垂-宸″洖锛氬悎璁¢檺棰濓紙itemid=43 涓旇垂鐢ㄨ鏄庡惈銆屽贰鍥炪�嶏級 */
+    private static final String CFG_OPERATION_ROOM_CIRCUIT = "sys.expertfee.operationroom.circuit.limit";
+    /** 鎵嬫湳瀹や汉宸ヨ垂-楹婚唹锛氬悎璁¢檺棰濓紙itemid=43 涓旇垂鐢ㄨ鏄庡惈銆岄夯閱夈�嶏級 */
+    private static final String CFG_OPERATION_ROOM_ANESTHESIA = "sys.expertfee.operationroom.anesthesia.limit";
+    /**
+     * 绗簩涓強浠ュ悗鐨勫櫒瀹樿幏鍙栵細鍙屼晶涓婇檺锛坕temid 32~39锛岃垂鐢ㄨ鏄庡惈銆屽弻渚с�嶏級銆�
+     * 璇ラ槇鍊兼棤娉曠敤 base_itemexpense 琛ㄨ揪锛堟爣鍑嗚〃閲岃鐨勬槸棣栦釜鍣ㄥ畼鐨勮冻棰濇爣鍑嗭級锛屾墍浠ユ斁 sys_config銆�
+     */
+    private static final String CFG_ORGAN_SECOND_DOUBLE = "sys.expertfee.organ.second.double.limit";
+    /** 绗簩涓強浠ュ悗鐨勫櫒瀹樿幏鍙栵細鍏朵粬锛堝崟渚с�佷互鍙婃棤鍗曞弻渚ф蹇电殑椤圭洰锛変笂闄� */
+    private static final String CFG_ORGAN_SECOND_OTHER = "sys.expertfee.organ.second.other.limit";
+
+    // ---- 璐圭敤椤圭洰 id 閰嶇疆锛堥�楀彿鍒嗛殧锛屽彲澶氫釜锛夈�傚叏閮ㄨ鍒欑粺涓�鎸夐」鐩� id 鍒ゅ畾锛�
+    //      鏈厤缃椂浠呴潬涓嬫柟鍘嗗彶 id 鍏煎锛屽搴旇鍒欎細璁� warn 鏃ュ織锛堜究浜庡彂鐜版紡閰嶏級 ----
+    private static final String CFG_ITEM_ORGAN_MAINTAIN = "sys.expertfee.itemid.organMaintain";
+    private static final String CFG_ITEM_SUPERIOR_MEDICAL = "sys.expertfee.itemid.superiorMedical";
+    private static final String CFG_ITEM_SUPERIOR_NURSING = "sys.expertfee.itemid.superiorNursing";
+    private static final String CFG_ITEM_ECOM = "sys.expertfee.itemid.ecom";
+    private static final String CFG_ITEM_OPERATION_ROOM = "sys.expertfee.itemid.operationRoom";
+    private static final String CFG_ITEM_ASSESS = "sys.expertfee.itemid.assess";
+    private static final String CFG_ITEM_ASSISTANT = "sys.expertfee.itemid.assistant";
+    private static final String CFG_ITEM_DOCTOR = "sys.expertfee.itemid.doctor";
+
+    // ---- 9 鏉¤鍒欐牎楠屾�诲紑鍏� ----
+    /**
+     * 涓撳鍔冲姟璐� 9 鏉¤鍒欐牎楠屾�诲紑鍏筹紝鍙栧�硷細1/true/y/on = 寮�鍚紝0/false/n/off = 鍏抽棴銆�
+     *
+     * <p>瑙勫垯浠嶅湪涓庝笟鍔℃柟纭涓紝2026-09-28 璧�<b>鏆傛椂鍏抽棴</b>锛�
+     * 瑙勫垯纭鍚庢妸璇ラ厤缃敼涓� true 鍗冲彲閲嶆柊寮�鍚紙鏃犻渶鏀逛唬鐮併�佹棤闇�鍙戠増锛夈��
+     */
+    private static final String CFG_VALIDATE_ENABLED = "sys.expertfee.validate.enabled";
+    /**
+     * 鎬诲紑鍏崇己鐪佸�硷細<b>鍏抽棴</b>銆�
+     *
+     * <p>缂虹渷鍙栧叧闂紝鍙繚璇佸嵆浣� sys_config 婕忛厤涔熶笉浼氳鎷︿笟鍔★紱
+     * 瑙勫垯纭鍚庨渶鏄惧紡閰嶇疆涓� true 鎵嶅紑鍚牎楠屻��
+     */
+    private static final boolean DEFAULT_VALIDATE_ENABLED = false;
+
+    // ---- 閰嶇疆缂虹渷鍊硷紙sys_config 鏈厤缃椂鍏滃簳锛岄伩鍏嶆牎楠屽け鏁堬級 ----
+    private static final BigDecimal DEFAULT_ORGAN_MAINTAIN = new BigDecimal("60000");
+    private static final BigDecimal DEFAULT_SUPERIOR_MEDICAL = new BigDecimal("11000");
+    private static final BigDecimal DEFAULT_SUPERIOR_NURSING = new BigDecimal("1000");
+    private static final BigDecimal DEFAULT_ECOM_IN_HOSPITAL = new BigDecimal("5000");
+    private static final BigDecimal DEFAULT_ECOM_IN_CITY = new BigDecimal("10000");
+    private static final BigDecimal DEFAULT_ECOM_OUT_CITY = new BigDecimal("12000");
+    private static final BigDecimal DEFAULT_ASSISTANT_IN_CITY = new BigDecimal("1000");
+    private static final BigDecimal DEFAULT_ASSISTANT_OUT_CITY = new BigDecimal("2000");
+    /** 鎵嬫湳瀹や汉宸ヨ垂-宸″洖鍚堣闄愰 */
+    private static final BigDecimal DEFAULT_OPERATION_ROOM_CIRCUIT = new BigDecimal("500");
+    /** 鎵嬫湳瀹や汉宸ヨ垂-楹婚唹鍚堣闄愰 */
+    private static final BigDecimal DEFAULT_OPERATION_ROOM_ANESTHESIA = new BigDecimal("500");
+    /** 绗簩涓強浠ュ悗鐨勫櫒瀹樿幏鍙栵細鍙屼晶涓婇檺缂虹渷鍊� */
+    private static final BigDecimal DEFAULT_ORGAN_SECOND_DOUBLE = new BigDecimal("1000");
+    /** 绗簩涓強浠ュ悗鐨勫櫒瀹樿幏鍙栵細鍏朵粬涓婇檺缂虹渷鍊� */
+    private static final BigDecimal DEFAULT_ORGAN_SECOND_OTHER = new BigDecimal("500");
+
+    @Autowired
+    private ServiceFundMapper serviceFundMapper;
+
+    @Autowired
+    private ServiceFunddetailMapper serviceFunddetailMapper;
+
+    /** 璐圭敤椤圭洰涓昏〃锛坆ase_financesubject锛夛紝鐢ㄤ簬鎸夐」鐩悕绉板姩鎬佽В鏋愰」鐩� id */
+    @Autowired
+    private BaseFinancesubjectMapper baseFinancesubjectMapper;
+
+    /** 璐圭敤鏍囧噯琛紙base_itemexpense锛夛細鍩烘湰瑙勫垯鎸夈�岄」鐩� id + 璐圭敤璇存槑銆嶅彇鏍囧噯閲戦 */
+    @Autowired
+    private BaseItemexpenseMapper baseItemexpenseMapper;
+
+    @Autowired
+    private ISysConfigService sysConfigService;
+
+    /** 椤圭洰 id -> 椤圭洰鍚嶇О锛堝甫缂撳瓨锛夛紝浠呯敤浜庢嫾鎺ュ彲璇荤殑閿欒鎻愮ず鏂囨 */
+    private volatile Map<Long, String> expertFeeItemNameCache;
+
+    /**
+     * 璐圭敤鏍囧噯缂撳瓨锛歩temid -> (璐圭敤璇存槑 -> 鏍囧噯閲戦)銆�
+     *
+     * <p>甯� 60 绉掕繃鏈熸椂闂达細鏍囧噯琛ㄦ槸鍙淮鎶ょ殑鍩虹鏁版嵁锛屾敼浜嗕笉蹇呴噸鍚嵆鍙敓鏁堬紝
+     * 鍚屾椂閬垮厤涓�娆℃牎楠岄噷鎸夐」鐩弽澶嶆煡搴撱��
+     */
+    private final Map<Long, ItemExpenseEntry> itemExpenseCache = new ConcurrentHashMap<>();
+
+    /** 璐圭敤鏍囧噯缂撳瓨鏈夋晥鏈燂紙姣锛� */
+    private static final long ITEM_EXPENSE_CACHE_MILLIS = 60 * 1000L;
+
+    /** 璐圭敤鏍囧噯缂撳瓨椤� */
+    private static final class ItemExpenseEntry {
+        private final Map<String, BigDecimal> standards;
+        private final long expireAt;
+
+        private ItemExpenseEntry(Map<String, BigDecimal> standards, long expireAt) {
+            this.standards = standards;
+            this.expireAt = expireAt;
+        }
+    }
+
+    // ==================================================================
+    // 瀵瑰鍏ュ彛
+    // ==================================================================
+
+    /**
+     * 鎶ラ攢鍗曞~鎶ヤ繚瀛樺墠鐨勬牎楠屻��
+     *
+     * <p>姝ゆ椂鏈鎻愪氦鐨勬槑缁嗗皻鏈惤搴擄紝闇�瑕佹妸銆屾湰娆℃彁浜ょ殑鏄庣粏銆嶄笌銆屾渚嬩笅宸茶惤搴撶殑鍏朵粬鍗曟嵁鏄庣粏銆�
+     * 鍚堝苟鍚庡垽瀹氾紱鏈鎻愪氦鐨勬槑缁嗕細鎸� id 瑕嗙洊搴撲腑鍚屼竴鏉¤褰曪紝閬垮厤閲嶅璁$畻銆�
+     *
+     * @param serviceFundVO 鏈鎻愪氦鐨勮〃鍗�
+     * @return 鏍¢獙缁撴灉锛坋rrors 闃绘柇淇濆瓨锛寃arnings 浠呮彁绀猴級
+     */
+    public ExpertFeeCheckResult validateBeforeSave(ServiceFundVO serviceFundVO) {
+        if (!isValidateEnabled()) {
+            log.info("[涓撳璐规牎楠�-淇濆瓨] 9 鏉¤鍒欐牎楠屾�诲紑鍏冲凡鍏抽棴锛坽}锛夛紝璺宠繃鏍¢獙",
+                    CFG_VALIDATE_ENABLED);
+            return new ExpertFeeCheckResult();
+        }
+        if (serviceFundVO == null) {
+            log.info("[涓撳璐规牎楠�-淇濆瓨] 鍏ュ弬涓虹┖锛岃烦杩�");
+            return new ExpertFeeCheckResult();
+        }
+        log.info("[涓撳璐规牎楠�-淇濆瓨] 寮�濮� applytype={} infoid={} fundId={} 鏈鏄庣粏鏁�={}",
+                serviceFundVO.getApplytype(), serviceFundVO.getInfoid(), serviceFundVO.getId(),
+                CollectionUtils.isEmpty(serviceFundVO.getServiceFunddetails())
+                        ? 0 : serviceFundVO.getServiceFunddetails().size());
+        if (!isExpertFee(serviceFundVO.getApplytype())) {
+            log.info("[涓撳璐规牎楠�-淇濆瓨] applytype={} 涓嶆槸涓撳鍔冲姟璐�(1)锛岃烦杩�", serviceFundVO.getApplytype());
+            return new ExpertFeeCheckResult();
+        }
+        Long infoid = serviceFundVO.getInfoid();
+        if (infoid == null) {
+            log.warn("[涓撳璐规牎楠�-淇濆瓨] infoid 涓虹┖锛屾棤娉曞仛妗堜緥鍐呯疮璁★紝璺宠繃锛坒undId={}锛�", serviceFundVO.getId());
+            return new ExpertFeeCheckResult();
+        }
+
+        List<Long> fundIds = listExpertFundIds(infoid);
+        List<DetailRow> storedRows = loadDetailRows(fundIds);
+        log.info("[涓撳璐规牎楠�-淇濆瓨] infoid={} 鍛戒腑涓撳璐瑰崟鎹� {} 寮狅紝搴撲腑鏄庣粏 {} 鏉�",
+                infoid, fundIds.size(), storedRows.size());
+
+        // 鐢ㄦ湰娆℃彁浜ょ殑鏄庣粏瑕嗙洊搴撲腑鍚� id 鐨勮褰曪紙鏂板鐨勮拷鍔犺繘鏉ワ級
+        Map<String, DetailRow> merged = new LinkedHashMap<>();
+        for (DetailRow row : storedRows) {
+            merged.put(rowKey(row), row);
+        }
+        List<ServiceFunddetailVO> submitDetails = serviceFundVO.getServiceFunddetails();
+        if (!CollectionUtils.isEmpty(submitDetails)) {
+            for (ServiceFunddetailVO vo : submitDetails) {
+                if (vo == null) {
+                    continue;
+                }
+                if (vo.getInfoid() == null) {
+                    vo.setInfoid(infoid);
+                }
+                merged.put(rowKey(vo), DetailRow.of(vo));
+            }
+        }
+
+        return doValidate(new ArrayList<>(merged.values()));
+    }
+
+    /**
+     * 鎶ラ攢鍗曞鏍搁�氳繃鍓嶇殑鏍¢獙銆傛暟鎹互鏁版嵁搴撲负鍑嗐��
+     *
+     * @param fundid 寰呭鏍哥殑鎶ラ攢鍗� id
+     * @return 鏍¢獙缁撴灉
+     */
+    public ExpertFeeCheckResult validateBeforeCheck(Long fundid) {
+        if (!isValidateEnabled()) {
+            log.info("[涓撳璐规牎楠�-瀹℃牳] 9 鏉¤鍒欐牎楠屾�诲紑鍏冲凡鍏抽棴锛坽}锛夛紝璺宠繃鏍¢獙",
+                    CFG_VALIDATE_ENABLED);
+            return new ExpertFeeCheckResult();
+        }
+        ServiceFund fund = serviceFundMapper.selectById(fundid);
+        if (fund == null || !isExpertFee(applytypeOf(fund.getApplytype()))) {
+            return new ExpertFeeCheckResult();
+        }
+        if (fund.getInfoid() == null) {
+            log.warn("涓撳鍔冲姟璐瑰鏍告牎楠岃烦杩囷細infoid 涓虹┖锛宖undId={}", fundid);
+            return new ExpertFeeCheckResult();
+        }
+        return doValidate(loadDetailRows(listExpertFundIds(fund.getInfoid())));
+    }
+
+    // ==================================================================
+    // 鏍¢獙涓讳綋
+    // ==================================================================
+
+    private ExpertFeeCheckResult doValidate(List<DetailRow> rows) {
+        ExpertFeeCheckResult result = new ExpertFeeCheckResult();
+        if (CollectionUtils.isEmpty(rows)) {
+            log.info("[涓撳璐规牎楠宂 鍙備笌鏍¢獙鐨勬槑缁嗕负绌猴紝鐩存帴閫氳繃");
+            return result;
+        }
+        log.info("[涓撳璐规牎楠宂 鍙備笌鏍¢獙鏄庣粏 {} 鏉★細{}", rows.size(), describeRows(rows));
+
+        // 瑙勫垯 1~4銆�7~9锛氬悎璁¢檺棰濈被锛堥樆鏂級锛屽叏閮ㄦ寜椤圭洰 id 鍒ゅ畾
+        checkSingleItemLimit(rows, resolveItemIds(CFG_ITEM_ORGAN_MAINTAIN, "鎹愮尞鍖婚櫌鍣ㄥ畼鍔熻兘缁存姢璐�", FALLBACK_ORGAN_MAINTAIN),
+                false,
+                cfg(CFG_ORGAN_MAINTAIN, DEFAULT_ORGAN_MAINTAIN), result, "鎹愮尞鍖婚櫌鍣ㄥ畼鍔熻兘缁存姢璐圭敤");
+        checkSingleItemLimit(rows, resolveItemIds(CFG_ITEM_SUPERIOR_MEDICAL, "涓婄骇鍖荤枟鍥㈤槦鍣ㄥ畼鍔熻兘缁存姢璐�", FALLBACK_SUPERIOR_MEDICAL),
+                true,
+                cfg(CFG_SUPERIOR_MEDICAL, DEFAULT_SUPERIOR_MEDICAL), result, "涓婄骇鎹愮尞鍖婚櫌鍖荤枟鍥㈤槦鍣ㄥ畼鍔熻兘缁存姢璐圭敤");
+        checkSingleItemLimit(rows, resolveItemIds(CFG_ITEM_SUPERIOR_NURSING, "涓婄骇鎶ょ悊鍥㈤槦鍣ㄥ畼鍔熻兘缁存姢璐�", FALLBACK_SUPERIOR_NURSING),
+                true,
+                cfg(CFG_SUPERIOR_NURSING, DEFAULT_SUPERIOR_NURSING), result, "涓婄骇鎹愮尞鍖婚櫌鎶ょ悊鍥㈤槦鍣ㄥ畼鍔熻兘缁存姢璐圭敤");
+        checkEcom(rows, result);
+        checkAssistant(rows, result);
+        checkOperationRoom(rows, result);
+
+        // 瑙勫垯 5锛氫粎鎻愮ず锛涘熀鏈鍒欎笌鍣ㄥ畼鑾峰彇鍔犳垚锛氱‖鎷︽埅
+        checkAssessConflict(rows, result);
+        checkItemStandard(rows, result);
+        checkDoctorOrganSecond(rows, result);
+
+        log.info("[涓撳璐规牎楠宂 鏍¢獙瀹屾垚 闃绘柇={} 鎻愮ず={} 闃绘柇淇℃伅={} 鎻愮ず淇℃伅={}",
+                result.getErrors().size(), result.getWarnings().size(),
+                result.getErrors(), result.getWarnings());
+        return result;
+    }
+
+    /** 鎶婂弬涓庢牎楠岀殑鏄庣粏鎷兼垚涓�琛岋紝渚夸簬鎺掓煡銆屼负浠�涔堟病鎷︿綇銆嶏紙閲戦銆侀」鐩�佸満鏅竴鐩簡鐒讹級 */
+    private String describeRows(List<DetailRow> rows) {
+        StringBuilder sb = new StringBuilder();
+        for (DetailRow row : rows) {
+            if (sb.length() > 0) {
+                sb.append(" | ");
+            }
+            sb.append("[fundid=").append(row.getFundid())
+                    .append(" itemid=").append(row.getItemid())
+                    .append(" 閲戦=").append(row.getAmount())
+                    .append(" 绋庡悗=").append(row.getTaxedamount())
+                    .append(" 鍦烘櫙=").append(row.getServicesscopename())
+                    .append("]");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 瑙勫垯 1銆�2銆�3锛氬崟涓�椤圭洰鍚堣闄愰銆�
+     *
+     * @param taxed 閲戦鏄惁鍙栫◣鍚庯紙瑙勫垯 2銆�3 鍙栫◣鍚庯紝鍏朵綑鍙栫◣鍓嶏級
+     */
+    private void checkSingleItemLimit(List<DetailRow> rows, Set<Long> itemids, boolean taxed,
+                                      BigDecimal limit, ExpertFeeCheckResult result, String itemLabel) {
+        List<DetailRow> matched = new ArrayList<>();
+        BigDecimal total = BigDecimal.ZERO;
+        for (DetailRow row : rows) {
+            if (isTargetItem(row, itemids)) {
+                matched.add(row);
+                total = total.add(taxed ? row.getTaxedamount() : row.getAmount());
+            }
+        }
+        if (matched.isEmpty()) {
+            return;
+        }
+        log.info("[涓撳璐规牎楠宂 {} 鍛戒腑{}鏉� 鍚堣={} 闄愰={} 鏄惁瓒呴檺={}",
+                itemLabel, matched.size(), stripZero(total), stripZero(limit), total.compareTo(limit) > 0);
+        if (total.compareTo(limit) > 0) {
+            result.addError(String.format(
+                    "%s鍚堣%s鍏冿紝瓒呰繃闄愰%s鍏冿紙妗堜緥鍐呯疮璁★紝鍏�%d鏉℃槑缁嗭級",
+                    itemLabel, stripZero(total), stripZero(limit), matched.size()));
+        }
+    }
+
+    /** 瑙勫垯 4锛欵COM鍥㈤槦鍔冲姟璐癸紝鎸夎垂鐢ㄨ鏄庡垎闄㈠唴/甯傚唴/甯傚涓夋。 */
+    private void checkEcom(List<DetailRow> rows, ExpertFeeCheckResult result) {
+        BigDecimal inHospital = BigDecimal.ZERO;
+        BigDecimal inCity = BigDecimal.ZERO;
+        BigDecimal outCity = BigDecimal.ZERO;
+        int count = 0;
+        int unknownScopeRows = 0;
+        Set<Long> ecomItemIds = resolveItemIds(CFG_ITEM_ECOM, "ECOM鍥㈤槦鍔冲姟璐�", FALLBACK_ECOM);
+        for (DetailRow row : rows) {
+            if (!isTargetItem(row, ecomItemIds)) {
+                continue;
+            }
+            count++;
+            if (row.scopeContainsAny(SCOPE_IN_HOSPITAL)) {
+                inHospital = inHospital.add(row.getAmount());
+            } else if (row.scopeContainsAny(SCOPE_OUT_CITY)) {
+                outCity = outCity.add(row.getAmount());
+            } else if (row.scopeContainsAny(SCOPE_IN_CITY)) {
+                inCity = inCity.add(row.getAmount());
+            } else if (row.getAmount().compareTo(BigDecimal.ZERO) > 0) {
+                // 鍦烘櫙鏃犳硶璇嗗埆鏃朵笉褰掑叆浠讳綍妗o紙閬垮厤璇垽锛夛紝浣嗕笉鑳介潤榛樻斁杩囷紝鎻愮ず缁忓姙浜烘牳瀹�
+                unknownScopeRows++;
+                log.warn("[涓撳璐规牎楠宂 ECOM鍥㈤槦鍔冲姟璐瑰瓨鍦ㄦ棤娉曡瘑鍒満鏅殑鏄庣粏锛歠undid={} itemid={} 璐圭敤璇存槑={} 閲戦={}",
+                        row.getFundid(), row.getItemid(), row.getServicesscopename(), row.getAmount());
+            }
+        }
+        if (count == 0) {
+            return;
+        }
+        if (unknownScopeRows > 0) {
+            result.addWarning(String.format(
+                    "ECOM鍥㈤槦鍔冲姟璐规湁%d鏉℃槑缁嗙殑璐圭敤璇存槑涓嶅湪銆岄櫌鍐�/甯傚唴/甯傚銆嶈寖鍥村唴锛屾湭绾冲叆闄愰鏍¢獙锛岃鏍稿疄",
+                    unknownScopeRows));
+        }
+        checkLimit(result, "ECOM鍥㈤槦鍔冲姟璐癸紙闄㈠唴锛�", inHospital, cfg(CFG_ECOM_IN_HOSPITAL, DEFAULT_ECOM_IN_HOSPITAL));
+        checkLimit(result, "ECOM鍥㈤槦鍔冲姟璐癸紙甯傚唴锛�", inCity, cfg(CFG_ECOM_IN_CITY, DEFAULT_ECOM_IN_CITY));
+        checkLimit(result, "ECOM鍥㈤槦鍔冲姟璐癸紙甯傚锛�", outCity, cfg(CFG_ECOM_OUT_CITY, DEFAULT_ECOM_OUT_CITY));
+    }
+
+    /**
+     * 瑙勫垯 7锛氬姪鎵嬭垂鐢ㄢ�斺�旀寜銆愬姪鎵嬬被鍨嬶紙椤圭洰 id锛夈�戝垎鍒悎璁★紝
+     * 姣忎釜绫诲瀷鍚勮嚜锛氬競鍐� 鈮� 1000銆佸甯� 鈮� 2000锛坕temid 41/71/72/73/74锛夈��
+     *
+     * <p>2026-09-30 闇�姹傚彉鏇达細鍘熷疄鐜�"涓嶅尯鍒嗗叿浣撴槸鍝釜鍣ㄥ畼/鍝釜鍔╂墜椤圭洰锛屾墍鏈夊姪鎵嬫槑缁嗗悎骞剁疮璁�"
+     * 锛堜竴涓競鍐呭悎璁� + 涓�涓甯傚悎璁★級锛屼笌鍘熷闇�姹傘�屽悓涓櫒瀹樺涓姪鎵嬪悎璁¢檺棰濄�嶄笉绗︼紝浼氬ぇ闈㈢Н璇嫤
+     * 锛堟湰鍦板簱 1004 涓惈鍔╂墜鏄庣粏鐨勬渚嬩腑锛屾棫鍙e緞鎷� 647 涓紝鎸夌被鍨嬪垎寮�鍚庡彧鎷� 75 涓級銆�
+     * 鐜版敼涓猴細鍚屼竴妗堜緥銆佸悓涓�鍔╂墜绫诲瀷鍐呯疮璁★紝涓嶅悓绫诲瀷涔嬮棿涓嶄簰鐩稿崰鐢ㄩ搴︼紱
+     * 甯傚唴澶栦粛鐢� servicesscopename锛堣垂鐢ㄨ鏄庯級鍒ゅ畾銆�
+     */
+    private void checkAssistant(List<DetailRow> rows, ExpertFeeCheckResult result) {
+        BigDecimal inCityLimit = cfg(CFG_ASSISTANT_IN_CITY, DEFAULT_ASSISTANT_IN_CITY);
+        BigDecimal outCityLimit = cfg(CFG_ASSISTANT_OUT_CITY, DEFAULT_ASSISTANT_OUT_CITY);
+        Set<Long> assistantItemIds = resolveItemIds(CFG_ITEM_ASSISTANT, "鍔╂墜璐圭敤", FALLBACK_ASSISTANT);
+
+        // key = itemid锛堝姪鎵嬬被鍨嬶級锛孡inkedHashMap + 鏈熬鎸� itemid 鎺掑簭锛屼繚璇佹彁绀洪『搴忕ǔ瀹�
+        Map<Long, AssistantTotal> groups = new LinkedHashMap<>();
+        int unknownScopeCount = 0;
+        for (DetailRow row : rows) {
+            if (!isAssistantItem(row, assistantItemIds)) {
+                continue;
+            }
+            boolean outCity = row.scopeContainsAny(SCOPE_OUT_CITY);
+            boolean inCity = !outCity && row.scopeContainsAny(SCOPE_IN_CITY);
+            if (!outCity && !inCity) {
+                // 鏈爣娉ㄥ競鍐呭鐨勪笉褰掑叆浠讳綍妗o紝閬垮厤璇垽锛堜粎璁版棩蹇楋紝涓嶆墦鎵颁笟鍔★級
+                unknownScopeCount++;
+                continue;
+            }
+            AssistantTotal group = groups.computeIfAbsent(row.getItemid(),
+                    key -> new AssistantTotal(key, itemNameOf(row)));
+            if (outCity) {
+                group.outCity = group.outCity.add(row.getAmount());
+                group.outCityCount++;
+            } else {
+                group.inCity = group.inCity.add(row.getAmount());
+                group.inCityCount++;
+            }
+        }
+
+        List<AssistantTotal> ordered = new ArrayList<>(groups.values());
+        ordered.sort((a, b) -> {
+            if (a.itemid == null) {
+                return b.itemid == null ? 0 : 1;
+            }
+            return b.itemid == null ? -1 : a.itemid.compareTo(b.itemid);
+        });
+
+        for (AssistantTotal group : ordered) {
+            log.info("[涓撳璐规牎楠宂 鍔╂墜璐圭敤[{}] 甯傚唴{}鏉″悎璁�={} 闄愰={}锛涘甯倇}鏉″悎璁�={} 闄愰={}",
+                    group.itemName, group.inCityCount, stripZero(group.inCity), stripZero(inCityLimit),
+                    group.outCityCount, stripZero(group.outCity), stripZero(outCityLimit));
+            if (group.inCityCount > 0 && group.inCity.compareTo(inCityLimit) > 0) {
+                result.addError(String.format(
+                        "鍔╂墜璐圭敤锛�%s锛屽競鍐咃級鍚堣%s鍏冿紝瓒呰繃闄愰%s鍏冿紙鍚屼竴妗堜緥涓嬭绫诲瀷鍔╂墜鍚堣锛屽叡%d鏉★級",
+                        group.itemName, stripZero(group.inCity), stripZero(inCityLimit), group.inCityCount));
+            }
+            if (group.outCityCount > 0 && group.outCity.compareTo(outCityLimit) > 0) {
+                result.addError(String.format(
+                        "鍔╂墜璐圭敤锛�%s锛屽甯傦級鍚堣%s鍏冿紝瓒呰繃闄愰%s鍏冿紙鍚屼竴妗堜緥涓嬭绫诲瀷鍔╂墜鍚堣锛屽叡%d鏉★級",
+                        group.itemName, stripZero(group.outCity), stripZero(outCityLimit), group.outCityCount));
+            }
+        }
+
+        if (unknownScopeCount > 0) {
+            log.warn("[涓撳璐规牎楠宂 鍔╂墜璐圭敤鏈墈}鏉℃槑缁嗙殑璐圭敤璇存槑鏃笉鍚競鍐�/鏈競涔熶笉鍚競澶�/澶栧競锛屾湭绾冲叆闄愰鏍¢獙",
+                    unknownScopeCount);
+        }
+    }
+
+    /** 瑙勫垯 7 鐨勭疮璁″崟鍏冿細鍚屼竴鍔╂墜绫诲瀷锛堥」鐩� id锛夊湪鍚屼竴妗堜緥涓嬬殑甯傚唴/澶栧競鍚堣 */
+    private static final class AssistantTotal {
+        private final Long itemid;
+        private final String itemName;
+        private BigDecimal inCity = BigDecimal.ZERO;
+        private int inCityCount;
+        private BigDecimal outCity = BigDecimal.ZERO;
+        private int outCityCount;
+
+        private AssistantTotal(Long itemid, String itemName) {
+            this.itemid = itemid;
+            this.itemName = itemName;
+        }
+    }
+
+    /**
+     * 瑙勫垯 8銆�9锛氭墜鏈浜哄伐璐规寜璐圭敤璇存槑鍒嗘。锛屽悇鑷嫭绔嬮檺棰� 鈥斺��
+     * 銆屽贰鍥炪�嶅悎璁� 鈮� 500銆併�岄夯閱夈�嶅悎璁� 鈮� 500锛坕temid=43锛夈��
+     *
+     * <p>2026-09-30 闇�姹傚彉鏇达細鍘熷疄鐜版妸宸″洖涓庨夯閱夊悎骞舵垚涓�涓� 500 鐨勬�婚檺棰濓紝
+     * 鐜版寜涓氬姟瑕佹眰鎷嗘垚涓ゆ。鍚勮嚜鏍¢獙锛堝宸″洖 300 + 楹婚唹 300 涓嶅啀鍒よ秴闄愶級銆�
+     *
+     * <p>璐圭敤璇存槑鏃笉鍚�屽贰鍥炪�嶄篃涓嶅惈銆岄夯閱夈�嶇殑鏄庣粏涓嶈鍏ヤ换浣曟。锛屼粎缁欐彁绀猴紱
+     * 鍚屾椂鍚袱涓瘝鐨勬槑缁嗗垎鍒鍏ヤ袱妗o紙骞惰 warn 鏃ュ織锛夈��
+     */
+    private void checkOperationRoom(List<DetailRow> rows, ExpertFeeCheckResult result) {
+        Set<Long> roomItemIds = resolveItemIds(CFG_ITEM_OPERATION_ROOM, "鎵嬫湳瀹や汉宸ヨ垂", FALLBACK_OPERATION_ROOM);
+
+        BigDecimal circuitTotal = BigDecimal.ZERO;
+        int circuitCount = 0;
+        BigDecimal anesthesiaTotal = BigDecimal.ZERO;
+        int anesthesiaCount = 0;
+        int unknownScopeCount = 0;
+        int bothScopeCount = 0;
+
+        for (DetailRow row : rows) {
+            if (!isTargetItem(row, roomItemIds)) {
+                continue;
+            }
+            boolean circuit = row.scopeContains(SCOPE_CIRCUIT);
+            boolean anesthesia = row.scopeContains(SCOPE_ANESTHESIA);
+            if (circuit && anesthesia) {
+                bothScopeCount++;
+            }
+            if (circuit) {
+                circuitTotal = circuitTotal.add(row.getAmount());
+                circuitCount++;
+            }
+            if (anesthesia) {
+                anesthesiaTotal = anesthesiaTotal.add(row.getAmount());
+                anesthesiaCount++;
+            }
+            if (!circuit && !anesthesia) {
+                unknownScopeCount++;
+            }
+        }
+
+        if (bothScopeCount > 0) {
+            log.warn("[涓撳璐规牎楠宂 鎵嬫湳瀹や汉宸ヨ垂鏈墈}鏉℃槑缁嗙殑璐圭敤璇存槑鍚屾椂鍚�屽贰鍥炪�嶄笌銆岄夯閱夈�嶏紝宸插垎鍒鍏ヤ袱妗�",
+                    bothScopeCount);
+        }
+
+        BigDecimal circuitLimit = cfg(CFG_OPERATION_ROOM_CIRCUIT, DEFAULT_OPERATION_ROOM_CIRCUIT);
+        if (circuitCount > 0) {
+            log.info("[涓撳璐规牎楠宂 鎵嬫湳瀹や汉宸ヨ垂(宸″洖) 鍛戒腑{}鏉� 鍚堣={} 闄愰={} 鏄惁瓒呴檺={}",
+                    circuitCount, stripZero(circuitTotal), stripZero(circuitLimit),
+                    circuitTotal.compareTo(circuitLimit) > 0);
+            if (circuitTotal.compareTo(circuitLimit) > 0) {
+                result.addError(String.format(
+                        "鎵嬫湳瀹や汉宸ヨ垂锛堝贰鍥烇級鍚堣%s鍏冿紝瓒呰繃闄愰%s鍏冿紙鍚屼竴妗堜緥涓嬪悎璁★紝鍏�%d鏉★級",
+                        stripZero(circuitTotal), stripZero(circuitLimit), circuitCount));
+            }
+        }
+
+        BigDecimal anesthesiaLimit = cfg(CFG_OPERATION_ROOM_ANESTHESIA, DEFAULT_OPERATION_ROOM_ANESTHESIA);
+        if (anesthesiaCount > 0) {
+            log.info("[涓撳璐规牎楠宂 鎵嬫湳瀹や汉宸ヨ垂(楹婚唹) 鍛戒腑{}鏉� 鍚堣={} 闄愰={} 鏄惁瓒呴檺={}",
+                    anesthesiaCount, stripZero(anesthesiaTotal), stripZero(anesthesiaLimit),
+                    anesthesiaTotal.compareTo(anesthesiaLimit) > 0);
+            if (anesthesiaTotal.compareTo(anesthesiaLimit) > 0) {
+                result.addError(String.format(
+                        "鎵嬫湳瀹や汉宸ヨ垂锛堥夯閱夛級鍚堣%s鍏冿紝瓒呰繃闄愰%s鍏冿紙鍚屼竴妗堜緥涓嬪悎璁★紝鍏�%d鏉★級",
+                        stripZero(anesthesiaTotal), stripZero(anesthesiaLimit), anesthesiaCount));
+            }
+        }
+
+        if (unknownScopeCount > 0) {
+            result.addWarning(String.format(
+                    "鎵嬫湳瀹や汉宸ヨ垂鏈�%d鏉℃槑缁嗙殑璐圭敤璇存槑鏃笉鍚�屽贰鍥炪�嶄篃涓嶅惈銆岄夯閱夈�嶏紝鏈撼鍏ラ檺棰濇牎楠岋紝璇锋牳瀹�",
+                    unknownScopeCount));
+        }
+    }
+
+    /**
+     * 瑙勫垯 5锛氬悓涓�涓撳涓嶅緱鍚屾椂鍦ㄣ�屾崘鐚尰闄㈠垵姝ヨ瘎浼拌垂鐢ㄣ�嶃�屾崘鐚梾鎯呰瘎浼颁笓瀹惰垂銆�
+     * 銆岃剳姝讳骸涓村簥鍒ゅ畾涓撳璐广�嶄腑鍑虹幇锛屽彧鑳藉氨楂樺彇鍏朵竴銆�
+     *
+     * <p>纭嫤鎴細杩濆弽鏃朵笉鍏佽淇濆瓨/瀹℃牳閫氳繃锛屼笉鑷姩鍒犻櫎涔熶笉鑷姩鏀规暟銆�
+     */
+    private void checkAssessConflict(List<DetailRow> rows, ExpertFeeCheckResult result) {
+        Set<Long> assessItemIds = resolveItemIds(CFG_ITEM_ASSESS, "璇勪及绫昏垂鐢�", FALLBACK_ASSESS);
+        Map<String, Map<Long, String>> expertItems = new LinkedHashMap<>();
+        for (DetailRow row : rows) {
+            if (!isTargetItem(row, assessItemIds)) {
+                continue;
+            }
+            String expertKey = expertKey(row);
+            if (expertKey == null) {
+                continue;
+            }
+            // 璁板綍鍛戒腑鐨勯」鐩強鍚嶇О锛屼究浜庢彁绀烘椂鍛婄煡鍏蜂綋鏄摢鍑犱釜椤圭洰
+            expertItems.computeIfAbsent(expertKey, k -> new LinkedHashMap<>())
+                    .put(row.getItemid(), itemNameOf(row));
+        }
+        for (Map.Entry<String, Map<Long, String>> entry : expertItems.entrySet()) {
+            if (entry.getValue().size() > 1) {
+                result.addError(String.format(
+                        "涓撳[%s]鍚屾椂鍦� %s 绛夊涓瘎浼扮被椤圭洰涓鍙栬垂鐢紝鍚屼竴涓撳鍙兘灏遍珮鍙栧叾涓�锛岃鍒犻櫎鍏朵綑鏉℃槑缁�",
+                        entry.getKey(), entry.getValue().values()));
+            }
+        }
+    }
+
+    // ==================================================================
+    // 鍩烘湰瑙勫垯锛�2026-10-08 鏂板锛夛細鍗曢」閲戦涓嶅緱瓒呰繃 base_itemexpense 鐨勮垂鐢ㄦ爣鍑�
+    // ==================================================================
+
+    /**
+     * 鍩烘湰瑙勫垯锛歛pplytype=1锛堜笓瀹跺姵鍔¤垂锛夌殑<b>鍏ㄩ儴鏄庣粏</b>閫愭潯涓庤垂鐢ㄦ爣鍑嗘瘮瀵广��
+     *
+     * <p>鍒ゆ嵁锛�2026-10-08 涓氬姟鏂圭‘璁わ級锛�
+     * <ol>
+     *   <li>鍚屼竴妗堜緥 脳 鍚屼竴涓撳 脳 鍚屼竴椤圭洰 脳 鍚屼竴璐圭敤璇存槑鐨�<b>鍚堣</b> 鈮� 璇ラ」鐩垂鐢ㄦ爣鍑嗭紱</li>
+     *   <li>閲戦涓� 0 鐨勬槑缁嗕竴寰嬮樆鏂紙鏄庣粏閲戦涓嶈兘涓� 0锛夛紱</li>
+     *   <li>璐圭敤璇存槑涓虹┖銆佹垨鍖归厤涓嶅埌鏍囧噯锛堝惈 UseState鈮�1銆佸凡鍒犻櫎锛夛紝涓�寰嬮樆鏂紱</li>
+     *   <li>鍚屼竴缁勫嚭鐜板鏉℃椂<b>鍙彁绀轰笉闃绘柇</b>锛堟槸鍚﹁秴闄愮敱涓婇潰鐨勫悎璁″垽瀹氾級銆�</li>
+     * </ol>
+     *
+     * <p>閲戦鍙e緞锛氳垂鐢ㄨ鏄庡惈銆岀◣鍚庛�嶇殑鍙�<b>绋庡悗閲戦</b>锛坱axedamount锛夛紝鍏朵綑鍙栫◣鍓嶉噾棰濓紙amount锛夈��
+     * 鏍囧噯琛ㄩ噷銆岀◣鍚庛�嶇被鏍囧噯鐨� Expense 鏈韩灏辨槸绋庡悗鍙e緞锛�
+     * 鑻ユ嬁绋庡墠閲戦锛堝惈绋庡姞浠峰悗鐨勫�硷級鍘绘瘮锛屼細鎶婂ぇ閲忔甯稿崟鎹鍒や负瓒呮爣鍑嗭紙瀹炴祴 3168 缁� vs 162 缁勶級銆�
+     */
+    private void checkItemStandard(List<DetailRow> rows, ExpertFeeCheckResult result) {
+        Map<String, List<DetailRow>> groups = new LinkedHashMap<>();
+        Map<String, BigDecimal> groupStandards = new LinkedHashMap<>();
+        Map<String, String> groupLabels = new LinkedHashMap<>();
+
+        for (DetailRow row : rows) {
+            BigDecimal amount = effectiveAmount(row);
+            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+                result.addError(String.format("%s閲戦涓�%s鍏冿紝鏄庣粏閲戦涓嶈兘涓�0锛岃濉啓閲戦鎴栧垹闄よ鏉℃槑缁�",
+                        itemLabel(row), stripZero(amount)));
+                continue;
+            }
+            String scope = scopeOf(row);
+            if (scope.isEmpty()) {
+                result.addError(String.format("%s鏈~鍐欒垂鐢ㄨ鏄庯紝鏃犳硶鏍¢獙璐圭敤鏍囧噯锛岃琛ュ厖璐圭敤璇存槑", itemLabel(row)));
+                continue;
+            }
+            BigDecimal standard = loadItemStandard(row.getItemid(), scope);
+            if (standard == null) {
+                result.addError(String.format(
+                        "%s鏈壘鍒板搴旂殑璐圭敤鏍囧噯锛堥」鐩甶d=%s锛岃垂鐢ㄨ鏄�=%s锛夛紝涓嶅厑璁告姤閿�锛岃鑱旂郴绠$悊鍛樼淮鎶よ垂鐢ㄦ爣鍑�",
+                        itemLabel(row), row.getItemid(), scope));
+                continue;
+            }
+            String key = groupKey(row, scope);
+            groups.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
+            groupStandards.putIfAbsent(key, standard);
+            groupLabels.putIfAbsent(key, groupLabel(row));
+        }
+
+        for (Map.Entry<String, List<DetailRow>> entry : groups.entrySet()) {
+            String key = entry.getKey();
+            List<DetailRow> list = entry.getValue();
+            BigDecimal total = BigDecimal.ZERO;
+            for (DetailRow row : list) {
+                total = total.add(effectiveAmount(row));
+            }
+            BigDecimal standard = groupStandards.get(key);
+            String label = groupLabels.get(key);
+            log.info("[涓撳璐规牎楠宂 鍩烘湰瑙勫垯 {} 鍛戒腑{}鏉� 鍚堣={} 鏍囧噯={} 鏄惁瓒呮爣={}",
+                    label, list.size(), stripZero(total), stripZero(standard),
+                    total.compareTo(standard) > 0);
+            if (total.compareTo(standard) > 0) {
+                result.addError(String.format(
+                        "%s鍚堣%s鍏冿紝瓒呰繃璐圭敤鏍囧噯%s鍏冿紙鍚屼竴妗堜緥鍚屼竴涓撳鍚屼竴椤圭洰鐨勭疮璁★紝鍏�%d鏉℃槑缁嗭級",
+                        label, stripZero(total), stripZero(standard), list.size()));
+            }
+            if (list.size() > 1) {
+                result.addWarning(String.format("%s鍦ㄥ悓涓�妗堜緥涓嬫湁%d鏉℃槑缁嗭紙鍚堣%s鍏冿級锛岃纭鏄惁閲嶅褰曞叆",
+                        label, list.size(), stripZero(total)));
+            }
+        }
+    }
+
+    /**
+     * 鍣ㄥ畼鑾峰彇鍔犳垚锛�2026-10-08 閲嶅啓锛屽彇浠e師銆屽悓涓�涓撳鍙厑璁镐竴鏉¤秴杩� 500銆嶏級銆�
+     *
+     * <p>鍚屼竴妗堜緥鍐咃紝鍚屼竴涓撳鍦ㄥ尰鐢熻幏鍙�/淇暣绫婚」鐩紙榛樿 itemid 32~39锛変笂鑾峰彇澶氫釜鍣ㄥ畼鏃讹細
+     * 鎸夐噾棰濋檷搴忎繚楂橈紝<b>绗簩涓強浠ュ悗鐨勬瘡鏉�</b>閲戦涓婇檺 = 璐圭敤璇存槑鍚�屽弻渚с�嶅彇 1000銆佸叾浠栧彇 500銆�
+     * 绗竴涓紙閲戦鏈�楂樼殑锛夐偅鏉℃寜璐圭敤鏍囧噯鏍¢獙锛岀敱涓婇潰鐨勩�屽熀鏈鍒欍�嶈礋璐c��
+     *
+     * <p>璇ラ槇鍊兼棤娉曠敤 base_itemexpense 琛ㄨ揪锛堟爣鍑嗚〃閲岃鐨勬槸棣栦釜鍣ㄥ畼鐨勮冻棰濇爣鍑嗭級锛屽洜姝ゆ斁 sys_config銆�
+     */
+    private void checkDoctorOrganSecond(List<DetailRow> rows, ExpertFeeCheckResult result) {
+        BigDecimal doubleSideLimit = cfg(CFG_ORGAN_SECOND_DOUBLE, DEFAULT_ORGAN_SECOND_DOUBLE);
+        BigDecimal otherLimit = cfg(CFG_ORGAN_SECOND_OTHER, DEFAULT_ORGAN_SECOND_OTHER);
+        Set<Long> doctorItemIds = resolveItemIds(CFG_ITEM_DOCTOR, "鍖荤敓鑾峰彇/淇暣绫昏垂鐢�", FALLBACK_DOCTOR_ORGAN);
+
+        Map<String, List<DetailRow>> byExpert = new LinkedHashMap<>();
+        for (DetailRow row : rows) {
+            if (!isDoctorOrganItem(row, doctorItemIds)) {
+                continue;
+            }
+            String expertKey = expertKey(row);
+            if (expertKey == null) {
+                continue;
+            }
+            byExpert.computeIfAbsent(expertKey, k -> new ArrayList<>()).add(row);
+        }
+
+        for (Map.Entry<String, List<DetailRow>> entry : byExpert.entrySet()) {
+            List<DetailRow> list = new ArrayList<>(entry.getValue());
+            if (list.size() < 2) {
+                continue;
+            }
+            // 閲戦闄嶅簭淇濋珮锛氭渶楂樼殑閭f潯鎸夎垂鐢ㄦ爣鍑嗭紙鍩烘湰瑙勫垯宸叉牎楠岋級锛屽叾浣欐寜 500/1000
+            list.sort((a, b) -> effectiveAmount(b).compareTo(effectiveAmount(a)));
+            log.info("[涓撳璐规牎楠宂 鍣ㄥ畼鑾峰彇鍔犳垚 涓撳={} 鍣ㄥ畼鑾峰彇鏄庣粏{}鏉� 涓婇檺(鍙屼晶/鍏朵粬)={}/{}",
+                    entry.getKey(), list.size(), stripZero(doubleSideLimit), stripZero(otherLimit));
+            for (int i = 1; i < list.size(); i++) {
+                DetailRow row = list.get(i);
+                BigDecimal amount = effectiveAmount(row);
+                BigDecimal limit = row.scopeContains(SCOPE_DOUBLE_SIDE) ? doubleSideLimit : otherLimit;
+                if (amount.compareTo(limit) > 0) {
+                    result.addError(String.format(
+                            "涓撳[%s]鐨勭%d涓櫒瀹樿幏鍙�%s閲戦%s鍏冿紝瓒呰繃涓婇檺%s鍏�"
+                                    + "锛堝悓涓�妗堜緥涓浜屼釜鍙婁互鍚庣殑鍣ㄥ畼鑾峰彇锛氬弻渚т笂闄�%s鍏冦�佸叾浠栦笂闄�%s鍏冿級锛岃璋冩暣閲戦",
+                            entry.getKey(), i + 1, itemLabel(row), stripZero(amount), stripZero(limit),
+                            stripZero(doubleSideLimit), stripZero(otherLimit)));
+                }
+            }
+        }
+    }
+
+    /** 鏄庣粏鐨勩�屾湁鏁堥噾棰濄�嶏細璐圭敤璇存槑鍚�岀◣鍚庛�嶅彇绋庡悗閲戦锛屽叾浣欏彇绋庡墠閲戦 */
+    private BigDecimal effectiveAmount(DetailRow row) {
+        return row.scopeContains(SCOPE_TAXED) ? row.getTaxedamount() : row.getAmount();
+    }
+
+    /** 鏄庣粏鐨勮垂鐢ㄨ鏄庯紙鍘荤┖鏍硷級 */
+    private String scopeOf(DetailRow row) {
+        return row.getServicesscopename() == null ? "" : row.getServicesscopename().trim();
+    }
+
+    /** 鍒嗙粍閿細涓撳 + 椤圭洰 + 璐圭敤璇存槑锛堟渚嬭寖鍥村凡鐢� rows 闄愬畾锛� */
+    private String groupKey(DetailRow row, String scope) {
+        String expert = row.getIdcardno() == null ? "" : row.getIdcardno().trim();
+        return expert + "|" + row.getItemid() + "|" + scope;
+    }
+
+    /** 鎻愮ず鏂囨锛氥�岄」鐩疯垂鐢ㄨ鏄庛�� */
+    private String itemLabel(DetailRow row) {
+        String scope = scopeOf(row);
+        return "銆�" + itemNameOf(row) + (scope.isEmpty() ? "" : "路" + scope) + "銆�";
+    }
+
+    /** 鎻愮ず鏂囨锛氫笓瀹禰韬唤璇乚銆岄」鐩疯垂鐢ㄨ鏄庛�� */
+    private String groupLabel(DetailRow row) {
+        String expert = row.getIdcardno() == null ? "" : row.getIdcardno().trim();
+        return (expert.isEmpty() ? "" : "涓撳[" + expert + "]") + itemLabel(row);
+    }
+
+    /**
+     * 鍙栥�岄」鐩� id + 璐圭敤璇存槑銆嶇殑璐圭敤鏍囧噯銆�
+     *
+     * <p>鏍囧噯琛ㄦ槸鍙淮鎶ょ殑鍩虹鏁版嵁锛氬彧璁� {@code del_flag = 0 涓� UseState = 1} 鐨勮褰�
+     * 锛堝疄浣撴病鏈� delFlag 瀛楁锛屽洜姝� del_flag 鐢� SQL 鐗囨闄愬畾锛夈��
+     *
+     * @return 鎵句笉鍒版椂杩斿洖 {@code null}锛岃皟鐢ㄦ柟鎸夈�屽尮閰嶄笉鍒版爣鍑嗐�嶉樆鏂�
+     */
+    private BigDecimal loadItemStandard(Long itemid, String scope) {
+        return loadItemStandards(itemid).get(scope);
+    }
+
+    /** 鎸夐」鐩� id 鍔犺浇璐圭敤鏍囧噯锛堢粨鏋滅紦瀛� 60 绉掞級锛宬ey = 璐圭敤璇存槑 */
+    private Map<String, BigDecimal> loadItemStandards(Long itemid) {
+        if (itemid == null) {
+            return Collections.emptyMap();
+        }
+        long now = System.currentTimeMillis();
+        ItemExpenseEntry cached = itemExpenseCache.get(itemid);
+        if (cached != null && cached.expireAt > now) {
+            return cached.standards;
+        }
+        Map<String, BigDecimal> standards = new LinkedHashMap<>();
+        try {
+            // 鐢ㄥ瓧绗︿覆鍒楀悕鍖呰鍣細BaseItemexpense 瀹炰綋娌℃湁 delFlag 瀛楁锛堟棤娉曠敤 lambda 寮曠敤锛夛紝
+            // 涓斿彲閬垮厤渚濊禆 MyBatis-Plus 鐨� lambda 缂撳瓨
+            QueryWrapper<BaseItemexpense> wrapper = new QueryWrapper<>();
+            wrapper.eq("ItemId", itemid);
+            wrapper.eq("UseState", "1");
+            wrapper.apply("del_flag = 0");
+            wrapper.orderByAsc("Id");
+            List<BaseItemexpense> list = baseItemexpenseMapper.selectList(wrapper);
+            if (list != null) {
+                for (BaseItemexpense item : list) {
+                    if (item == null || item.getExpensedescribe() == null || item.getExpense() == null) {
+                        continue;
+                    }
+                    standards.putIfAbsent(item.getExpensedescribe().trim(), item.getExpense());
+                }
+            }
+        } catch (Exception e) {
+            log.error("[涓撳璐规牎楠宂 璇诲彇璐圭敤鏍囧噯澶辫触锛宨temid={}", itemid, e);
+        }
+        itemExpenseCache.put(itemid, new ItemExpenseEntry(standards, now + ITEM_EXPENSE_CACHE_MILLIS));
+        return standards;
+    }
+
+    // ==================================================================
+    // 璐圭敤椤圭洰鍔ㄦ�佽В鏋�
+    //
+    // 鑳屾櫙锛氳垂鐢ㄩ」鐩〃 base_financesubject 鐨勯」鐩寜骞村害閲嶅缓锛堜緥锛氣�滐紙2026锛夋崘鐚尰闄㈠櫒瀹樺姛鑳界淮鎶よ垂鐢ㄢ��
+    // 鏄� id=75锛岃�屾棭鏈熷勾搴︿负 5锛夛紝鍥犳涓嶈兘鎶� itemid 鍐欐鍦ㄤ唬鐮侀噷銆�
+    // 鍋氭硶锛氭寜椤圭洰鍚嶇О鍏抽敭璇嶅埌鍩虹琛ㄨВ鏋愬綋鍓嶆湁鏁堢殑椤圭洰 id锛屽啀涓庘�滄棭鏈熷勾搴� id鈥濇眰骞堕泦銆�
+    // ==================================================================
+
+    /**
+     * 瑙f瀽瑙勫垯瀵瑰簲鐨勮垂鐢ㄩ」鐩� id 闆嗗悎銆�
+     *
+     * <p>浼樺厛绾э細
+     * <ol>
+     *   <li>sys_config 涓樉寮忛厤缃殑椤圭洰 id锛堥�楀彿鍒嗛殧锛夆�斺�旈」鐩悕绉颁笉瑙勮寖鏃剁敤杩欎釜鍏滃簳</li>
+     *   <li>鎸夐」鐩悕绉板叧閿瘝鍒板熀纭�琛ㄨВ鏋愶紙鑷姩閫傚簲椤圭洰骞村害閲嶅缓锛�</li>
+     *   <li>鏃╂湡骞村害鐨勫巻鍙查」鐩� id</li>
+     * </ol>
+     * 浠呮寜椤圭洰 id 鍒ゅ畾锛氬彇 sys_config 涓厤缃殑椤圭洰 id 鍒楄〃锛屼笌鍘嗗彶骞村害 id 姹傚苟闆嗐��
+     * 閰嶇疆涓虹┖涓旀棤鍘嗗彶 id 鏃惰繑鍥炵┖闆嗭紝姝ゆ椂璇ヨ鍒欎笉鐢熸晥骞朵細璁� warn 鏃ュ織銆�
+     *
+     * @param itemIdConfigKey sys_config 涓」鐩� id 鍒楄〃鐨勯厤缃敭
+     * @param ruleLabel       瑙勫垯鍚嶇О锛屼粎鐢ㄤ簬鏃ュ織鍙鎬�
+     * @param fallback        鍘嗗彶骞村害锛堥」鐩〃鏀归�犲墠锛変娇鐢ㄧ殑椤圭洰 id
+     */
+    private Set<Long> resolveItemIds(String itemIdConfigKey, String ruleLabel, Set<Long> fallback) {
+        Set<Long> ids = new LinkedHashSet<>();
+        ids.addAll(parseItemIdConfig(itemIdConfigKey));
+        if (fallback != null) {
+            ids.addAll(fallback);
+        }
+        if (ids.isEmpty()) {
+            log.warn("[涓撳璐规牎楠宂 瑙勫垯[{}]鏈厤缃」鐩甶d锛坰ys_config.{}锛夛紝璇ヨ鍒欐湰娆′笉鐢熸晥",
+                    ruleLabel, itemIdConfigKey);
+        }
+        return ids;
+    }
+
+    /** 璇诲彇 sys_config 涓�楀彿鍒嗛殧鐨勯」鐩� id 鍒楄〃锛堟湭閰嶇疆鎴栨牸寮忛敊璇椂杩斿洖绌洪泦锛� */
+    private Set<Long> parseItemIdConfig(String configKey) {
+        Set<Long> ids = new LinkedHashSet<>();
+        if (configKey == null) {
+            return ids;
+        }
+        try {
+            String value = sysConfigService.selectConfigByKey(configKey);
+            if (StringUtils.isEmpty(value)) {
+                return ids;
+            }
+            for (String part : value.split(",")) {
+                String trimmed = part.trim();
+                if (StringUtils.isEmpty(trimmed)) {
+                    continue;
+                }
+                try {
+                    ids.add(Long.valueOf(trimmed));
+                } catch (NumberFormatException e) {
+                    log.warn("[涓撳璐规牎楠宂 椤圭洰id閰嶇疆[{}]涓瓨鍦ㄩ潪娉曞�尖�渰}鈥濓紝宸插拷鐣�", configKey, trimmed);
+                }
+            }
+        } catch (Exception e) {
+            log.warn("[涓撳璐规牎楠宂 璇诲彇椤圭洰id閰嶇疆澶辫触锛宬ey={}", configKey, e);
+        }
+        return ids;
+    }
+
+    /** 鏄庣粏琛屾槸鍚﹀睘浜庣粰瀹氶」鐩� id 闆嗗悎锛堝叏閮ㄨ鍒欑粺涓�鎸夐」鐩� id 鍒ゅ畾锛� */
+    private boolean isTargetItem(DetailRow row, Set<Long> itemids) {
+        return !CollectionUtils.isEmpty(itemids)
+                && row.getItemid() != null
+                && itemids.contains(row.getItemid());
+    }
+
+    /** 瑙勫垯 6锛氬尰鐢熲�淴X鑾峰彇/淇暣鈥濈被椤圭洰锛堜粎鎸夐」鐩� id锛� */
+    private boolean isDoctorOrganItem(DetailRow row, Set<Long> itemids) {
+        return isTargetItem(row, itemids);
+    }
+
+    /** 瑙勫垯 7锛氬姪鎵嬬被椤圭洰锛堜粎鎸夐」鐩� id锛� */
+    private boolean isAssistantItem(DetailRow row, Set<Long> itemids) {
+        return isTargetItem(row, itemids);
+    }
+
+    /** 鍙栨槑缁嗙殑椤圭洰鍚嶇О锛堟槑缁嗘湭鎼哄甫鏃跺洖鏌ュ熀纭�琛級锛屼粎鐢ㄤ簬鎻愮ず鏂囨 */
+    private String itemNameOf(DetailRow row) {
+        String name = row.getItemname();
+        if (StringUtils.hasText(name)) {
+            return name;
+        }
+        if (row.getItemid() != null) {
+            String cached = loadExpertFeeItemNames().get(row.getItemid());
+            if (StringUtils.hasText(cached)) {
+                return cached;
+            }
+        }
+        return "itemid=" + row.getItemid();
+    }
+
+    /** 椤圭洰 id -> 椤圭洰鍚嶇О锛堝甫缂撳瓨锛夛紝浠呯敤浜庢槑缁嗘湭鎼哄甫鍚嶇О鏃跺洖鏌ャ�佹嫾鎺ユ彁绀烘枃妗� */
+    private Map<Long, String> loadExpertFeeItemNames() {
+        Map<Long, String> cache = this.expertFeeItemNameCache;
+        if (cache != null) {
+            return cache;
+        }
+        synchronized (this) {
+            if (this.expertFeeItemNameCache == null) {
+                Map<Long, String> names = new LinkedHashMap<>();
+                for (BaseFinancesubject subject : loadExpertFeeItems()) {
+                    if (subject.getId() != null && subject.getItemname() != null) {
+                        names.put(subject.getId(), subject.getItemname());
+                    }
+                }
+                this.expertFeeItemNameCache = names;
+                cache = names;
+            } else {
+                cache = this.expertFeeItemNameCache;
+            }
+        }
+        return cache;
+    }
+
+    /** 璇诲彇涓撳鍔冲姟璐癸紙璐圭敤澶х被=1锛夌殑鍏ㄩ儴鍦ㄧ敤椤圭洰 */
+    private List<BaseFinancesubject> loadExpertFeeItems() {
+        List<BaseFinancesubject> items = baseFinancesubjectMapper.getAllInfoByExpenseType(APPLYTYPE_EXPERT);
+        return CollectionUtils.isEmpty(items) ? new ArrayList<>() : items;
+    }
+
+    /** 瀛楅潰閲忛泦鍚堟瀯閫狅紙渚� static final 甯搁噺浣跨敤锛� */
+    private static Set<Long> setOf(Long... values) {
+        Set<Long> set = new LinkedHashSet<>();
+        if (values != null) {
+            for (Long value : values) {
+                set.add(value);
+            }
+        }
+        return set;
+    }
+
+    // ==================================================================
+    // 宸ュ叿鏂规硶
+    // ==================================================================
+
+    private void checkLimit(ExpertFeeCheckResult result, String label, BigDecimal total, BigDecimal limit) {
+        if (total.compareTo(BigDecimal.ZERO) > 0 && total.compareTo(limit) > 0) {
+            result.addError(String.format("%s鍚堣%s鍏冿紝瓒呰繃闄愰%s鍏冿紙妗堜緥鍐呯疮璁★級",
+                    label, stripZero(total), stripZero(limit)));
+        }
+    }
+
+    /** 鍙栬妗堜緥涓嬪叏閮ㄤ笓瀹跺姵鍔¤垂鎶ラ攢鍗曠殑 id */
+    private List<Long> listExpertFundIds(Long infoid) {
+        List<ServiceFund> funds = serviceFundMapper.getInfoByInfoId(infoid);
+        if (CollectionUtils.isEmpty(funds)) {
+            return Collections.emptyList();
+        }
+        return funds.stream()
+                .filter(f -> f != null && isExpertFee(applytypeOf(f.getApplytype())))
+                .map(ServiceFund::getId)
+                .filter(java.util.Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
+    /** 鎸夋姤閿�鍗� id 鍔犺浇鏄庣粏锛圶ML 涓凡甯� del_flag = '0'锛� */
+    private List<DetailRow> loadDetailRows(List<Long> fundIds) {
+        if (CollectionUtils.isEmpty(fundIds)) {
+            return Collections.emptyList();
+        }
+        List<DetailRow> rows = new ArrayList<>();
+        for (Long fundId : fundIds) {
+            List<ServiceFunddetail> details = serviceFunddetailMapper.selectFundDetailListById(fundId);
+            if (CollectionUtils.isEmpty(details)) {
+                continue;
+            }
+            for (ServiceFunddetail detail : details) {
+                DetailRow row = new DetailRow();
+                row.setId(detail.getId());
+                row.setFundid(detail.getFundid());
+                row.setInfoid(detail.getInfoid());
+                row.setItemid(detail.getItemid());
+                row.setItemname(detail.getItemname());
+                row.setIdcardno(detail.getIdcardno());
+                row.setServicesscopename(detail.getServicesscopename());
+                row.setAmount(toDecimal(detail.getAmount()));
+                row.setTaxedamount(toDecimal(detail.getTaxedamount()));
+                rows.add(row);
+            }
+        }
+        return rows;
+    }
+
+    /**
+     * 鏄庣粏琛岀殑鍞竴閿��
+     *
+     * <p>宸茶惤搴撶殑鏄庣粏鐢ㄤ富閿� id锛涙柊澧烇紙灏氭棤 id锛夌殑鐢�
+     * 銆屾姤閿�鍗� + 椤圭洰 + 韬唤璇� + 璐圭敤璇存槑 + 閲戦銆嶇粍鍚堬紝
+     * 纭繚鍚屼竴椤圭洰涓嬬殑澶氳鏄庣粏涓嶄細琚悎骞讹紙鍚﹀垯浼氭紡绠楁垨閲嶅璁$畻锛夈��
+     */
+    private String rowKey(DetailRow row) {
+        if (row.getId() != null) {
+            return "id:" + row.getId();
+        }
+        return "new:" + row.getFundid() + "|" + row.getItemid() + "|" + row.getIdcardno()
+                + "|" + row.getServicesscopename() + "|" + row.getAmount();
+    }
+
+    private String rowKey(ServiceFunddetailVO vo) {
+        if (vo.getId() != null) {
+            return "id:" + vo.getId();
+        }
+        // 鏂板鏄庣粏灏氭棤 id锛氱敤銆屾姤閿�鍗� + 椤圭洰 + 韬唤璇� + 璐圭敤璇存槑 + 閲戦銆嶈繎浼煎畾浣嶏紝淇濊瘉鍚屼竴椤圭洰澶氳涓嶄細琚悎骞�
+        return "new:" + vo.getFundid() + "|" + vo.getItemid() + "|" + vo.getIdcardno()
+                + "|" + vo.getServicesscopename() + "|" + vo.getAmount();
+    }
+
+    /** 涓撳鐨勮瘑鍒敭锛氫紭鍏堣韩浠借瘉鍙凤紝鍏舵鍙楃泭浜虹紪鍙凤紝閮芥病鏈夋椂璺宠繃璇ヨ */
+    private String expertKey(DetailRow row) {
+        if (StringUtils.hasText(row.getIdcardno())) {
+            return row.getIdcardno().trim();
+        }
+        return null;
+    }
+
+    private boolean isExpertFee(String applytype) {
+        return APPLYTYPE_EXPERT.equals(applytype);
+    }
+
+    private String applytypeOf(String applytype) {
+        return applytype;
+    }
+
+    /**
+     * 9 鏉¤鍒欐牎楠屾槸鍚﹀紑鍚��
+     *
+     * <p>鍏抽棴鏃舵墍鏈夊叆鍙g洿鎺ヨ繑鍥�"閫氳繃"锛堟棦涓嶅仛搴撴煡璇紝涔熶笉浜х敓 errors/warnings锛夛紝
+     * 鍓嶇棰勬牎楠屻�佷繚瀛樸�佸鏍镐笁鏉¢摼璺悓鏃舵斁琛屻��
+     */
+    private boolean isValidateEnabled() {
+        try {
+            String value = sysConfigService.selectConfigByKey(CFG_VALIDATE_ENABLED);
+            if (StringUtils.hasText(value)) {
+                String v = value.trim();
+                return "1".equals(v) || "true".equalsIgnoreCase(v)
+                        || "y".equalsIgnoreCase(v) || "on".equalsIgnoreCase(v);
+            }
+        } catch (Exception e) {
+            log.warn("璇诲彇涓撳鍔冲姟璐规牎楠屾�诲紑鍏冲け璐ワ紝key={}锛屾寜榛樿鍊�({})澶勭悊",
+                    CFG_VALIDATE_ENABLED, DEFAULT_VALIDATE_ENABLED, e);
+        }
+        return DEFAULT_VALIDATE_ENABLED;
+    }
+
+    private BigDecimal cfg(String key, BigDecimal defaultValue) {
+        try {
+            String value = sysConfigService.selectConfigByKey(key);
+            if (StringUtils.hasText(value)) {
+                return new BigDecimal(value.trim());
+            }
+        } catch (Exception e) {
+            log.warn("璇诲彇涓撳鍔冲姟璐归檺棰濋厤缃け璐ワ紝key={}锛屼娇鐢ㄩ粯璁ゅ�納}", key, defaultValue, e);
+        }
+        return defaultValue;
+    }
+
+    private static BigDecimal toDecimal(Double value) {
+        return value == null ? BigDecimal.ZERO : BigDecimal.valueOf(value);
+    }
+
+    /** 鍘绘帀閲戦鏈熬澶氫綑鐨� 0锛屼究浜庢彁绀烘枃妗堥槄璇伙紙60000.00 -> 60000锛� */
+    private static String stripZero(BigDecimal value) {
+        return value.stripTrailingZeros().toPlainString();
+    }
+}

--
Gitblit v1.9.3