yxh
20 小时以前 8c215c8be0f5076e8c95daf1b2203440ca7dfcad
修改了报销校验
已修改19个文件
402 ■■■■■ 文件已修改
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceDonatecompletioninfoController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceDonatecomporganController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceDonateorganController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceDonationwitnessorganController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceEthicalreviewopinionsController.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceFunddetailController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceMedicalevaluationController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceReimbursementdetailController.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceRelativesconfirmationController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/domain/BaseItemexpense.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ExpertFeeValidateService.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceDonatebaseinfoServiceImpl.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceDonatecompletioninfoServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceDonationwitnessServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceEthicalreviewopinionsServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFundServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceMedicalevaluationServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceRelativesconfirmationServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/resources/mapper/project/ServiceFundMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceDonatecompletioninfoController.java
@@ -179,6 +179,11 @@
    @Log(title = "捐献完成", businessType = BusinessType.UPDATE)
    @PostMapping("/edit")
    public AjaxResult edit(@RequestBody ServiceDonatecompletioninfo serviceDonatecompletioninfo) {
        // 优先按主键更新(前端已回填 id 的常规路径)
        if (serviceDonatecompletioninfo.getId() != null) {
            return toAjax(serviceDonatecompletioninfoService.updateById(serviceDonatecompletioninfo));
        }
        // 兼容未传 id 的旧调用:按 infoid 定位未删除记录
        ServiceDonatecompletioninfo serviceDonatecompletioninfo1 = new ServiceDonatecompletioninfo();
        serviceDonatecompletioninfo1.setInfoid(serviceDonatecompletioninfo.getInfoid());
        List<ServiceDonatecompletioninfo> list = serviceDonatecompletioninfoService.queryList(serviceDonatecompletioninfo1);
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceDonatecomporganController.java
@@ -178,6 +178,7 @@
                }
            }
        }
        return toAjax(i);
        // 回传带主键的列表,便于前端回填 id
        return AjaxResult.success(serviceDonatecomporganList);
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceDonateorganController.java
@@ -297,13 +297,23 @@
        Boolean i = null;
        for (ServiceDonateorgan serviceDonateorgan : serviceDonateorganList) {
            log.info("捐献器官移植id是否为空 : {},基础表的id为 : {}", serviceDonateorgan.getId(), serviceDonateorgan.getInfoid());
            // 2026-10-22:同一案例、同一器官只保留一条未删除记录——已存在则更新,避免重复插入
            ServiceDonateorgan query = new ServiceDonateorgan();
            query.setInfoid(serviceDonateorgan.getInfoid());
            query.setOrganno(serviceDonateorgan.getOrganno());
            query.setDel_flag(0);
            List<ServiceDonateorgan> exist = serviceDonateorganService.queryList(query);
            if (!CollectionUtils.isEmpty(exist)) {
                serviceDonateorgan.setId(exist.get(0).getId());
            }
            if (serviceDonateorgan.getId() == null) {
                i = serviceDonateorganService.save(serviceDonateorgan);
            } else {
                i = serviceDonateorganService.updateById(serviceDonateorgan);
            }
        }
        return toAjax(i);
        // 回传带主键的列表,便于前端回填 id
        return AjaxResult.success(serviceDonateorganList);
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceDonationwitnessorganController.java
@@ -134,6 +134,7 @@
                i = serviceDonationwitnessorganService.updateById(serviceDonationwitnessorgan);
            }
        }
        return toAjax(i);
        // 回传带主键的列表,便于前端回填 id
        return AjaxResult.success(serviceDonationwitnessorganList);
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceEthicalreviewopinionsController.java
@@ -74,10 +74,21 @@
     */
    //// @PreAuthorize("@ss.hasPermi('project:ethicalreviewopinions:add')")
    @Log(title = "伦理审查专家意见", businessType = BusinessType.INSERT)
    @RepeatSubmit
    @PostMapping("/add")
//    @NotRepeatCommit(key = "param:arg[1]", value = 30000)
    public AjaxResult add(@RequestBody ServiceEthicalreviewopinions serviceEthicalreviewopinions) {
        boolean save = serviceEthicalreviewopinionsService.save(serviceEthicalreviewopinions);
        // 2026-10-22:同一案例只允许一条未删除记录——已存在则更新,避免反复保存产生重复行
        if (serviceEthicalreviewopinions.getInfoid() != null) {
            ServiceEthicalreviewopinions query = new ServiceEthicalreviewopinions();
            query.setInfoid(serviceEthicalreviewopinions.getInfoid());
            List<ServiceEthicalreviewopinions> exist = serviceEthicalreviewopinionsService.queryList(query);
            if (exist != null && !exist.isEmpty()) {
                serviceEthicalreviewopinions.setId(exist.get(0).getId());
                serviceEthicalreviewopinionsService.updateById(serviceEthicalreviewopinions);
                return AjaxResult.success(serviceEthicalreviewopinions);
            }
        }
        serviceEthicalreviewopinionsService.save(serviceEthicalreviewopinions);
        return AjaxResult.success(serviceEthicalreviewopinions);
    }
@@ -87,7 +98,7 @@
    //// @PreAuthorize("@ss.hasPermi('project:ethicalreviewopinions:edit')")
    @Log(title = "伦理审查专家意见", businessType = BusinessType.UPDATE)
    @PostMapping("/edit")
//    @RepeatSubmit
    @RepeatSubmit
    public AjaxResult edit(@RequestBody ServiceEthicalreviewopinions serviceEthicalreviewopinions) {
        return toAjax(serviceEthicalreviewopinionsService.updateById(serviceEthicalreviewopinions));
    }
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceFunddetailController.java
@@ -87,6 +87,13 @@
    @PostMapping("/add")
    @NotRepeatCommit(key = "param:arg[1]", value = 30000)
    public AjaxResult add(@RequestBody ServiceFunddetail serviceFunddetail) {
        // 2026-10-15:附件文件名规范二次校验(与前端 opo-web/src/utils/fileNameCheck.js 同口径,
        // 目的是防止绕过前端直接调接口保存不合规附件名)。新增场景没有历史附件,用严格模式。
        String nameError = com.ruoyi.project.common.AttachmentNameValidator
                .checkJsonList(serviceFunddetail.getInvoicefiles(), serviceFunddetail.getAnnexfiles());
        if (nameError != null) {
            return AjaxResult.error("附件文件名不符合要求,无法保存:" + nameError);
        }
        boolean save = serviceFunddetailService.save(serviceFunddetail);
        return AjaxResult.success(serviceFunddetail);
    }
@@ -101,6 +108,24 @@
    @NotRepeatCommit(key = "param:arg[1]", value = 30000)
    public AjaxResult edit(@RequestBody ServiceFunddetail serviceFunddetail) {
        log.info("修改的数据信息:{}", serviceFunddetail);
        // 2026-10-15:附件文件名规范二次校验。修改场景按业务口径"只校验本次变更的附件":
        // 保存前该明细已存在的历史附件名不参与校验,否则老单据(历史不合规名占比很高)改个金额都存不进去。
        java.util.Set<String> existingNames = new java.util.HashSet<>();
        if (serviceFunddetail.getId() != null) {
            ServiceFunddetail oldDetail = serviceFunddetailService.getById(serviceFunddetail.getId());
            if (oldDetail != null) {
                existingNames.addAll(com.ruoyi.project.common.AttachmentNameValidator
                        .collectNames(oldDetail.getInvoicefiles()));
                existingNames.addAll(com.ruoyi.project.common.AttachmentNameValidator
                        .collectNames(oldDetail.getAnnexfiles()));
            }
        }
        String nameError = com.ruoyi.project.common.AttachmentNameValidator
                .checkJsonListForUpdate(existingNames, serviceFunddetail.getInvoicefiles(),
                        serviceFunddetail.getAnnexfiles());
        if (nameError != null) {
            return AjaxResult.error("附件文件名不符合要求,无法保存:" + nameError);
        }
        return toAjax(serviceFunddetailService.updateById(serviceFunddetail));
    }
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceMedicalevaluationController.java
@@ -128,10 +128,21 @@
    @ApiOperation("新增医学评估")
    //// @PreAuthorize("@ss.hasPermi('project:medicalevaluation:add')")
    @Log(title = "医学评估", businessType = BusinessType.INSERT)
    @RepeatSubmit
    @PostMapping("/add")
//    @NotRepeatCommit(key = "param:arg[1]", value = 30000)
    public AjaxResult add(@RequestBody ServiceMedicalevaluation serviceMedicalevaluation) {
        boolean save = serviceMedicalevaluationService.save(serviceMedicalevaluation);
        // 2026-10-22:同一案例只允许一条未删除记录——已存在则更新,避免反复保存产生重复行
        if (serviceMedicalevaluation.getInfoid() != null) {
            ServiceMedicalevaluation query = new ServiceMedicalevaluation();
            query.setInfoid(serviceMedicalevaluation.getInfoid());
            List<ServiceMedicalevaluation> exist = serviceMedicalevaluationService.queryList(query);
            if (exist != null && !exist.isEmpty()) {
                serviceMedicalevaluation.setId(exist.get(0).getId());
                serviceMedicalevaluationService.updateById(serviceMedicalevaluation);
                return AjaxResult.success(serviceMedicalevaluation);
            }
        }
        serviceMedicalevaluationService.save(serviceMedicalevaluation);
        return AjaxResult.success(serviceMedicalevaluation);
    }
@@ -141,9 +152,14 @@
    @ApiOperation("修改医学评估")
    //// @PreAuthorize("@ss.hasPermi('project:medicalevaluation:edit')")
    @Log(title = "医学评估", businessType = BusinessType.UPDATE)
    @RepeatSubmit
    @PostMapping("/edit")
//    @NotRepeatCommit(key = "param:arg[1]", value = 30000)
    public AjaxResult edit(@RequestBody ServiceMedicalevaluation serviceMedicalevaluation) {
        // 优先按主键更新(前端已回填 id 的常规路径)
        if (serviceMedicalevaluation.getId() != null) {
            return toAjax(serviceMedicalevaluationService.updateById(serviceMedicalevaluation));
        }
        // 兼容未传 id 的旧调用:按 infoid 定位未删除记录
        List<ServiceMedicalevaluation> list = serviceMedicalevaluationService.queryList(serviceMedicalevaluation);
        if (CollectionUtils.isEmpty(list)) {
            return error("医学评估数据不存在,请检查后再次修改");
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceReimbursementdetailController.java
@@ -102,6 +102,21 @@
    @PostMapping("/add")
    @RepeatSubmit
    public AjaxResult add(@RequestBody ServiceReimbursementdetailVO serviceReimbursementdetailVO) {
        // 2026-10-15:附件文件名规范二次校验(与前端 opo-web/src/utils/fileNameCheck.js 同口径,
        // 目的是防止绕过前端直接调接口保存不合规附件名)。新增场景没有历史附件,用严格模式。
        // 注意:List 为空时必须回退校验字符串字段(annexfiles / invoicefiles),
        // 否则只传字符串字段就能绕过校验(接口层面的绕过口)。
        String invoiceJson = CollectionUtils.isEmpty(serviceReimbursementdetailVO.getInvoicefilesList())
                ? serviceReimbursementdetailVO.getInvoicefiles()
                : JSON.toJSONString(serviceReimbursementdetailVO.getInvoicefilesList());
        String annexJson = CollectionUtils.isEmpty(serviceReimbursementdetailVO.getAnnexfilesList())
                ? serviceReimbursementdetailVO.getAnnexfiles()
                : JSON.toJSONString(serviceReimbursementdetailVO.getAnnexfilesList());
        String nameError = com.ruoyi.project.common.AttachmentNameValidator
                .checkJsonList(invoiceJson, annexJson);
        if (nameError != null) {
            return AjaxResult.error("附件文件名不符合要求,无法保存:" + nameError);
        }
        ServiceReimbursementdetail serviceReimbursementdetail = DtoConversionUtils.sourceToTarget(serviceReimbursementdetailVO, ServiceReimbursementdetail.class);
        //将前端传来的附件集合转成字符串
        if (!CollectionUtils.isEmpty(serviceReimbursementdetailVO.getAnnexfilesList())) {
@@ -123,6 +138,24 @@
    @PostMapping("/updateRDlList")
    public AjaxResult updateRDlList(@RequestBody List<ServiceReimbursementdetail> serviceReimbursementdetails) {
        for (ServiceReimbursementdetail serviceReimbursementdetail : serviceReimbursementdetails) {
            // 2026-10-15:附件文件名规范二次校验(只校验本次变更的附件,历史附件放行)
            java.util.Set<String> existingNames = new java.util.HashSet<>();
            if (serviceReimbursementdetail.getId() != null) {
                ServiceReimbursementdetail oldDetail = serviceReimbursementdetailService
                        .getById(serviceReimbursementdetail.getId());
                if (oldDetail != null) {
                    existingNames.addAll(com.ruoyi.project.common.AttachmentNameValidator
                            .collectNames(oldDetail.getInvoicefiles()));
                    existingNames.addAll(com.ruoyi.project.common.AttachmentNameValidator
                            .collectNames(oldDetail.getAnnexfiles()));
                }
            }
            String nameError = com.ruoyi.project.common.AttachmentNameValidator.checkJsonListForUpdate(
                    existingNames, serviceReimbursementdetail.getInvoicefiles(),
                    serviceReimbursementdetail.getAnnexfiles());
            if (nameError != null) {
                return AjaxResult.error("附件文件名不符合要求,无法保存:" + nameError);
            }
            serviceReimbursementdetailService.saveOrUpdate(serviceReimbursementdetail);
        }
@@ -138,6 +171,31 @@
    @PostMapping("/reimbursementdetailEdit")
    //@RepeatSubmit
    public AjaxResult edit(@RequestBody ServiceReimbursementdetailVO serviceReimbursementdetailVO) {
        // 2026-10-15:附件文件名规范二次校验(与前端 fileNameCheck 同口径,防止绕过前端)。
        // 修改场景按业务口径"只校验本次变更的附件":保存前已存在的历史附件名不参与校验,避免卡住老单据。
        java.util.Set<String> existingNames = new java.util.HashSet<>();
        if (serviceReimbursementdetailVO.getId() != null) {
            ServiceReimbursementdetail oldDetail = serviceReimbursementdetailService
                    .getById(serviceReimbursementdetailVO.getId());
            if (oldDetail != null) {
                existingNames.addAll(com.ruoyi.project.common.AttachmentNameValidator
                        .collectNames(oldDetail.getInvoicefiles()));
                existingNames.addAll(com.ruoyi.project.common.AttachmentNameValidator
                        .collectNames(oldDetail.getAnnexfiles()));
            }
        }
        // List 为空时回退校验字符串字段(annexfiles / invoicefiles),堵住"只传字符串字段"的绕过口
        String invoiceJson = CollectionUtils.isEmpty(serviceReimbursementdetailVO.getInvoicefilesList())
                ? serviceReimbursementdetailVO.getInvoicefiles()
                : JSON.toJSONString(serviceReimbursementdetailVO.getInvoicefilesList());
        String annexJson = CollectionUtils.isEmpty(serviceReimbursementdetailVO.getAnnexfilesList())
                ? serviceReimbursementdetailVO.getAnnexfiles()
                : JSON.toJSONString(serviceReimbursementdetailVO.getAnnexfilesList());
        String nameError = com.ruoyi.project.common.AttachmentNameValidator
                .checkJsonListForUpdate(existingNames, invoiceJson, annexJson);
        if (nameError != null) {
            return AjaxResult.error("附件文件名不符合要求,无法保存:" + nameError);
        }
        if (serviceReimbursementdetailVO.getTrafficexpense() == null) {
            serviceReimbursementdetailVO.setTrafficexpense(new BigDecimal(0));
        }
ruoyi-admin/src/main/java/com/ruoyi/web/controller/project/ServiceRelativesconfirmationController.java
@@ -122,7 +122,18 @@
    @RepeatSubmit
    public AjaxResult add(@RequestBody ServiceRelativesconfirmation serviceRelativesconfirmation) {
        log.info("新增捐献亲属确认:{}", serviceRelativesconfirmation);
        boolean save = serviceRelativesconfirmationService.save(serviceRelativesconfirmation);
        // 2026-10-22:同一案例只允许一条未删除记录——已存在则更新,避免反复保存产生重复行
        if (serviceRelativesconfirmation.getInfoid() != null) {
            ServiceRelativesconfirmation query = new ServiceRelativesconfirmation();
            query.setInfoid(serviceRelativesconfirmation.getInfoid());
            List<ServiceRelativesconfirmation> exist = serviceRelativesconfirmationService.queryList(query);
            if (exist != null && !exist.isEmpty()) {
                serviceRelativesconfirmation.setId(exist.get(0).getId());
                serviceRelativesconfirmationService.updateById(serviceRelativesconfirmation);
                return AjaxResult.success(serviceRelativesconfirmation);
            }
        }
        serviceRelativesconfirmationService.save(serviceRelativesconfirmation);
        return AjaxResult.success(serviceRelativesconfirmation);
    }
ruoyi-project/src/main/java/com/ruoyi/project/domain/BaseItemexpense.java
@@ -47,5 +47,13 @@
    @Excel(name = "启用状态")
    private String usestate;
    /**
     * 专家费校验模式:1=强制校验(金额类问题不通过则不能保存);2=校验提醒(金额类问题仅提醒,允许保存);3=不校验(跳过金额类规则)。
     * 数据质量类规则(金额为 0、未填写费用说明)在三种模式下都强制。
     */
    @ApiModelProperty("专家费校验模式:1=强制校验 2=校验提醒 3=不校验")
    @Excel(name = "校验模式")
    private Integer validateMode;
}
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ExpertFeeValidateService.java
@@ -305,6 +305,65 @@
        }
        log.info("[专家费校验] 参与校验明细 {} 条:{}", rows.size(), describeRows(rows));
        // 2026-10-15:按"费用项目"的校验模式(base_itemexpense.validate_mode)拆分明细后分别校验
        //   1=强制校验:金额类问题 → 阻断(error)
        //   2=校验提醒:金额类问题 → 仅提醒(warning),允许保存
        //   3=不校验  :跳过该项目全部金额类规则
        // 说明:金额类规则全部按项目 id 判定,同一项目的明细才会互相产生合计类问题,
        //       因此按项目拆分不会影响各规则内部逻辑;含跨项目比较的规则(如评估冲突)
        //       在双方都是强制模式时仍然一起参与,行为不变。
        List<DetailRow> forcedRows = new ArrayList<>();
        List<DetailRow> remindRows = new ArrayList<>();
        List<DetailRow> skippedRows = new ArrayList<>();
        for (DetailRow row : rows) {
            int mode = loadItemMode(row.getItemid());
            if (mode == MODE_SKIP) {
                skippedRows.add(row);
            } else if (mode == MODE_REMIND) {
                remindRows.add(row);
            } else {
                forcedRows.add(row);
            }
        }
        log.info("[专家费校验] 校验模式统计:强制={} 提醒={} 不校验={}",
                forcedRows.size(), remindRows.size(), skippedRows.size());
        // 1) 强制校验:正常阻断
        runAmountRules(forcedRows, result);
        // 2) 校验提醒:同样跑一遍,但把结果降级为提醒(不阻断)
        if (!remindRows.isEmpty()) {
            ExpertFeeCheckResult remindResult = new ExpertFeeCheckResult();
            runAmountRules(remindRows, remindResult);
            for (String error : remindResult.getErrors()) {
                // 业务方确认的提醒文案:金额类问题在「校验提醒」模式下不阻断,仅提示后可继续保存
                result.addWarning("该费用项存在超标准情况,仅作提醒,是否继续保存?【" + error + "】");
            }
            if (!remindResult.getErrors().isEmpty()) {
                log.info("[专家费校验] 以下明细所属项目为「校验提醒」模式,仅提醒不阻断:{}", describeRows(remindRows));
            }
        }
        // 3) 不校验:跳过金额类规则,仅记录日志,便于事后回溯
        if (!skippedRows.isEmpty()) {
            log.info("[专家费校验] 以下明细所属项目为「不校验」模式,已跳过金额类规则:{}", describeRows(skippedRows));
        }
        // 4) 数据质量类规则(金额为 0、未填写费用说明)在三种模式下都强制:
        //    对未参与强制校验的明细单独跑一次"只查数据质量"的检查,避免出现金额为空的脏数据
        checkItemStandard(remindRows, result, false);
        checkItemStandard(skippedRows, result, false);
        log.info("[专家费校验] 校验完成 阻断={} 提示={} 阻断信息={} 提示信息={}",
                result.getErrors().size(), result.getWarnings().size(),
                result.getErrors(), result.getWarnings());
        return result;
    }
    /**
     * 金额类规则(受"校验模式"影响):合计限额、ECOM、助手、手术室、评估冲突、费用标准、器官获取加成。
     */
    private void runAmountRules(List<DetailRow> rows, ExpertFeeCheckResult result) {
        // 规则 1~4、7~9:合计限额类(阻断),全部按项目 id 判定
        checkSingleItemLimit(rows, resolveItemIds(CFG_ITEM_ORGAN_MAINTAIN, "捐献医院器官功能维护费", FALLBACK_ORGAN_MAINTAIN),
                false,
@@ -321,13 +380,8 @@
        // 规则 5:仅提示;基本规则与器官获取加成:硬拦截
        checkAssessConflict(rows, result);
        checkItemStandard(rows, result);
        checkItemStandard(rows, result, true);
        checkDoctorOrganSecond(rows, result);
        log.info("[专家费校验] 校验完成 阻断={} 提示={} 阻断信息={} 提示信息={}",
                result.getErrors().size(), result.getWarnings().size(),
                result.getErrors(), result.getWarnings());
        return result;
    }
    /** 把参与校验的明细拼成一行,便于排查「为什么没拦住」(金额、项目、场景一目了然) */
@@ -626,6 +680,18 @@
     * 若拿税前金额(含税加价后的值)去比,会把大量正常单据误判为超标准(实测 3168 组 vs 162 组)。
     */
    private void checkItemStandard(List<DetailRow> rows, ExpertFeeCheckResult result) {
        checkItemStandard(rows, result, true);
    }
    /**
     * 费用标准校验。
     *
     * @param amountCheck {@code true}=完整校验(含金额类规则,受项目校验模式影响);
     *                    {@code false}=只查数据质量(金额为 0、未填写费用说明),
     *                    用于「校验提醒 / 不校验」模式下对未参与强制校验的明细做兜底强制,
     *                    避免出现金额为空的脏数据。
     */
    private void checkItemStandard(List<DetailRow> rows, ExpertFeeCheckResult result, boolean amountCheck) {
        Map<String, List<DetailRow>> groups = new LinkedHashMap<>();
        Map<String, BigDecimal> groupStandards = new LinkedHashMap<>();
        Map<String, String> groupLabels = new LinkedHashMap<>();
@@ -644,9 +710,17 @@
            }
            BigDecimal standard = loadItemStandard(row.getItemid(), scope);
            if (standard == null) {
                if (!amountCheck) {
                    // 「未找到费用标准」属金额类问题,按项目校验模式处理
                    continue;
                }
                result.addError(String.format(
                        "%s未找到对应的费用标准(项目id=%s,费用说明=%s),不允许报销,请联系管理员维护费用标准",
                        itemLabel(row), row.getItemid(), scope));
                continue;
            }
            if (!amountCheck) {
                // 只查数据质量:不再做费用标准比较与重复录入提醒
                continue;
            }
            String key = groupKey(row, scope);
@@ -765,6 +839,64 @@
     *
     * @return 找不到时返回 {@code null},调用方按「匹配不到标准」阻断
     */
    /** 校验模式:强制校验(金额类问题不通过则不能保存) */
    private static final int MODE_FORCE = 1;
    /** 校验模式:校验提醒(金额类问题仅提醒,允许保存) */
    private static final int MODE_REMIND = 2;
    /** 校验模式:不校验(跳过该项目的金额类规则) */
    private static final int MODE_SKIP = 3;
    /** 项目校验模式缓存(60 秒),key = itemid */
    private final Map<Long, Integer> itemModeCache = new java.util.concurrent.ConcurrentHashMap<>();
    private final Map<Long, Long> itemModeExpireCache = new java.util.concurrent.ConcurrentHashMap<>();
    /**
     * 读取费用项目的校验模式({@code base_itemexpense.validate_mode}),缓存 60 秒。
     *
     * <p>读不到、取值非法或查询异常时**一律按「强制校验」处理**,
     * 保证不会因为开关读不到而放松校验(宁可多拦,不可漏拦)。
     */
    private int loadItemMode(Long itemid) {
        if (itemid == null) {
            return MODE_FORCE;
        }
        long now = System.currentTimeMillis();
        Integer cached = itemModeCache.get(itemid);
        Long expireAt = itemModeExpireCache.get(itemid);
        if (cached != null && expireAt != null && expireAt > now) {
            return cached;
        }
        int mode = MODE_FORCE;
        try {
            // 2026-10-15(修复):不能用 wrapper.select("ValidateMode") + selectMaps 取这一列,两处都会让模式取不到:
            //   ① 字符串列名会被 MyBatis-Plus 原样拼进 SELECT,而真实列名是 validate_mode,
            //      MySQL 直接报 Unknown column 'ValidateMode',异常被下面的 catch 吞掉后永远回落「强制校验」;
            //   ② 即使把列名改成 validate_mode,selectMaps 返回的 Map key 也是原始列标签(validate_mode,
            //      不会转成驼峰),与 "validatemode" 匹配不上,模式依然取不到。
            // 改为与同类 loadItemStandards 一致的实体映射写法(经实体字段 validateMode 映射到列 validate_mode)。
            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 (!CollectionUtils.isEmpty(list) && list.get(0) != null
                    && list.get(0).getValidateMode() != null) {
                mode = list.get(0).getValidateMode();
            }
            if (mode != MODE_FORCE && mode != MODE_REMIND && mode != MODE_SKIP) {
                log.warn("[专家费校验] 项目校验模式取值非法,按强制校验处理,itemid={},validate_mode={}", itemid, mode);
                mode = MODE_FORCE;
            }
        } catch (Exception e) {
            log.error("[专家费校验] 读取项目校验模式失败,按强制校验处理,itemid={}", itemid, e);
            mode = MODE_FORCE;
        }
        itemModeCache.put(itemid, mode);
        itemModeExpireCache.put(itemid, now + ITEM_EXPENSE_CACHE_MILLIS);
        return mode;
    }
    private BigDecimal loadItemStandard(Long itemid, String scope) {
        return loadItemStandards(itemid).get(scope);
    }
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceDonatebaseinfoServiceImpl.java
@@ -216,6 +216,8 @@
    public List<ServiceDonatebaseinfo> selectServiceDonatebaseinfoList(ServiceDonatebaseinfo serviceDonatebaseinfo) {
        SysUser user = SecurityUtils.getLoginUser().getUser();
        // 超级管理员不受"只看本人报送 / 只看本部门"的数据范围限制,可查看全部数据
        if (!isSuperAdmin(user)) {
        List<SysRole> l = user.getRoles();
        int b = 0;
@@ -234,6 +236,7 @@
        }
        if (b == 2) {
            serviceDonatebaseinfo.setDeptid(user.getDeptId());
            }
        }
        List<ServiceDonatebaseinfo> serviceDonatebaseinfos = serviceDonatebaseinfoMapper.selectServiceDonatebaseinfoList(serviceDonatebaseinfo);
@@ -282,6 +285,8 @@
    public List<ServiceDonatebaseinfo> getPatName(ServiceDonatebaseinfo serviceDonatebaseinfo) {
        SysUser user = SecurityUtils.getLoginUser().getUser();
        // 超级管理员不受数据范围限制,可查看全部数据
        if (!isSuperAdmin(user)) {
        List<SysRole> l = user.getRoles();
        int b = 0;
@@ -300,6 +305,7 @@
        if (b == 2) {
            serviceDonatebaseinfo.setDeptid(user.getDeptId());
        }
        }
        List<ServiceDonatebaseinfo> serviceDonatebaseinfos = serviceDonatebaseinfoMapper.getPatName(serviceDonatebaseinfo);
        return serviceDonatebaseinfos;
@@ -308,6 +314,8 @@
    @Override
    public List<ServiceDonatebaseinfo> listForSearch(ServiceDonatebaseinfo serviceDonatebaseinfo) {
        SysUser user = SecurityUtils.getLoginUser().getUser();
        // 超级管理员不受数据范围限制,可查看全部数据
        if (!isSuperAdmin(user)) {
        List<SysRole> l = user.getRoles();
        Boolean b = false;
        for (SysRole r : l) {
@@ -317,6 +325,7 @@
        }
        if (b) {
            serviceDonatebaseinfo.setCreateBy(user.getUserName());
            }
        }
        return serviceDonatebaseinfoMapper.listForSearch(serviceDonatebaseinfo);
@@ -581,4 +590,33 @@
        //将数据封闭,并返回
        return map;
    }
    /**
     * 是否超级管理员。
     *
     * <p>判定为超级管理员的情形:
     * <ul>
     *   <li>内置管理员账号(userId = 1);</li>
     *   <li>账号被授予"超级管理员"角色(roleId = 1)。</li>
     * </ul>
     * 超级管理员不受"只看本人报送(角色3 专职人员)/ 只看本部门(角色13 业务组长)"的数据范围限制,
     * 可查看全部数据。
     */
    private boolean isSuperAdmin(SysUser user) {
        if (user == null) {
            return false;
        }
        if (SysUser.isAdmin(user.getUserId())) {
            return true;
        }
        List<SysRole> roles = user.getRoles();
        if (roles != null) {
            for (SysRole role : roles) {
                if (role.getRoleId() != null && role.getRoleId().longValue() == 1L) {
                    return true;
                }
            }
        }
        return false;
    }
}
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceDonatecompletioninfoServiceImpl.java
@@ -74,6 +74,12 @@
        if (StringUtils.isNotNull(serviceDonatecompletioninfo.getInfoid())) {
            wrappers.eq(ServiceDonatecompletioninfo::getInfoid, serviceDonatecompletioninfo.getInfoid());
        }
        // 2026-10-22:默认只查询未删除记录
        if (StringUtils.isNotNull(serviceDonatecompletioninfo.getDel_flag())) {
            wrappers.eq(ServiceDonatecompletioninfo::getDel_flag, serviceDonatecompletioninfo.getDel_flag());
        } else {
            wrappers.eq(ServiceDonatecompletioninfo::getDel_flag, 0);
        }
        return this.list(wrappers);
    }
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceDonationwitnessServiceImpl.java
@@ -73,6 +73,12 @@
        if (StringUtils.isNotBlank(serviceDonationwitness.getOperationdoctor())) {
            wrappers.eq(ServiceDonationwitness::getOperationdoctor, serviceDonationwitness.getOperationdoctor());
        }
        // 2026-10-22:默认只查询未删除记录
        if (StringUtils.isNotNull(serviceDonationwitness.getDel_flag())) {
            wrappers.eq(ServiceDonationwitness::getDel_flag, serviceDonationwitness.getDel_flag());
        } else {
            wrappers.eq(ServiceDonationwitness::getDel_flag, 0);
        }
        return this.list(wrappers);
    }
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceEthicalreviewopinionsServiceImpl.java
@@ -81,6 +81,12 @@
        if (serviceEthicalreviewopinions.getConclusionorder() != null) {
            wrappers.eq(ServiceEthicalreviewopinions::getConclusionorder, serviceEthicalreviewopinions.getConclusionorder());
        }
        // 2026-10-22:默认只查询未删除记录
        if (StringUtils.isNotNull(serviceEthicalreviewopinions.getDel_flag())) {
            wrappers.eq(ServiceEthicalreviewopinions::getDel_flag, serviceEthicalreviewopinions.getDel_flag());
        } else {
            wrappers.eq(ServiceEthicalreviewopinions::getDel_flag, 0);
        }
        return this.list(wrappers);
    }
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFundServiceImpl.java
@@ -1583,6 +1583,28 @@
            // 保存详情数据
            for (ServiceFunddetailVO serviceFunddetailVO : serviceFunddetails) {
                // 2026-10-15:附件文件名规范二次校验(与前端 fileNameCheck 同口径;只校验本次变更的附件,历史附件放行)
                java.util.Set<String> existingNames = new java.util.HashSet<>();
                if (serviceFunddetailVO.getId() != null) {
                    ServiceFunddetail oldDetail = serviceFunddetailMapper.selectById(serviceFunddetailVO.getId());
                    if (oldDetail != null) {
                        existingNames.addAll(com.ruoyi.project.common.AttachmentNameValidator
                                .collectNames(oldDetail.getInvoicefiles()));
                        existingNames.addAll(com.ruoyi.project.common.AttachmentNameValidator
                                .collectNames(oldDetail.getAnnexfiles()));
                    }
                }
                String invoiceJson = CollectionUtils.isEmpty(serviceFunddetailVO.getInvoicefilesList())
                        ? serviceFunddetailVO.getInvoicefiles()
                        : JSON.toJSONString(serviceFunddetailVO.getInvoicefilesList());
                String annexJson = CollectionUtils.isEmpty(serviceFunddetailVO.getAnnexfilesList())
                        ? serviceFunddetailVO.getAnnexfiles()
                        : JSON.toJSONString(serviceFunddetailVO.getAnnexfilesList());
                String nameError = com.ruoyi.project.common.AttachmentNameValidator
                        .checkJsonListForUpdate(existingNames, invoiceJson, annexJson);
                if (nameError != null) {
                    throw new BaseException("附件文件名不符合要求,无法保存:" + nameError);
                }
                //去掉身份证的首尾空格
                if (StringUtils.isNotEmpty(serviceFunddetailVO.getIdcardno())) {
                    serviceFunddetailVO.setIdcardno(serviceFunddetailVO.getIdcardno().trim());
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceMedicalevaluationServiceImpl.java
@@ -54,6 +54,12 @@
        if (StringUtils.isNotBlank(serviceMedicalevaluation.getDonorno())) {
            wrappers.eq(ServiceMedicalevaluation::getDonorno, serviceMedicalevaluation.getDonorno());
        }
        // 2026-10-22:默认只查询未删除记录
        if (StringUtils.isNotNull(serviceMedicalevaluation.getDel_flag())) {
            wrappers.eq(ServiceMedicalevaluation::getDel_flag, serviceMedicalevaluation.getDel_flag());
        } else {
            wrappers.eq(ServiceMedicalevaluation::getDel_flag, 0);
        }
        return this.list(wrappers);
    }
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceRelativesconfirmationServiceImpl.java
@@ -185,6 +185,12 @@
        if (serviceRelativesconfirmation.getUpdateTime() != null) {
            wrappers.eq(ServiceRelativesconfirmation::getUpdateTime, serviceRelativesconfirmation.getUpdateTime());
        }
        // 2026-10-22:默认只查询未删除记录
        if (StringUtils.isNotNull(serviceRelativesconfirmation.getDel_flag())) {
            wrappers.eq(ServiceRelativesconfirmation::getDel_flag, serviceRelativesconfirmation.getDel_flag());
        } else {
            wrappers.eq(ServiceRelativesconfirmation::getDel_flag, 0);
        }
        return this.list(wrappers);
    }
ruoyi-project/src/main/resources/mapper/project/ServiceFundMapper.xml
@@ -142,6 +142,8 @@
        where del_flag = '0'
        <if test="donorno != null  and donorno != ''">and DonorNo = #{donorno}</if>
        <!-- 2026-10-22:报销单列表支持按"捐献者姓名"模糊筛选(仅在传值时生效,不影响其它调用方) -->
        <if test="donorname != null  and donorname != ''">and DonorName like concat('%', #{donorname}, '%')</if>
        <if test="fundTaxId != null ">and fund_tax_id = #{fundTaxId}</if>
        <if test="userno != null  and userno != ''">and UserNo = #{userno}</if>
        <if test="username != null  and username != ''">and UserName like concat('%', #{username}, '%')</if>