xiaoyi
7 天以前 730f9b0c25a6c111dab1ea4a1ac5e1da9a48f765
fix: 修复全量接口扫描发现的 500(29 个降至 0,member 模块按要求跳过)

基于 /v3/api-docs 全量探测 1262 个 GET 接口,发现 29 个返回 500,逐类修复:

1. 数据库缺表/缺字段(2 个)
- 新建 hrm_social_security_config 表(建表语句原已存在于 sql/mysql/hrm_deduction_init.sql,线上未执行)
- mes_wm_outsource_receipt_line 补 batch_id / batch_code / production_date / expire_date / lot_number 五列

2. NPE(3 个)
- CrmContractConfigDO.getNotifyDays() 为 null 时 plusDays() 拆箱 NPE:Service 层缺省 7 天、Mapper 层 null 按 0 天双保险
- TranslateUtils.transService 为 null(Spring Boot 4 下 easy-trans 自动配置被禁用):translate 改为 null 时直接返回原数据
- list-role-menus 的 roleId 未声明必填,null 作为缓存 key 抛 Null key returned for cache operation:补 @RequestParam 走 400

3. 参数缺失按 400 处理(8 个)
- CRM 7 处 Assert.notNull 改为抛业务异常 BAD_REQUEST
- IM group-member/get 参数缺失同理改为 BAD_REQUEST

4. Excel 导出缺转换器(2 个)
- 新增 LocalTimeStringConverter(FastExcel 内置无 LocalTime)
- 新增 ListJsonStringConverter(FastExcel 内置无 List,按 JSON 文本输出)
- 两个转换器同时注册到 write 与 writeTemplate

5. BPM / 接口文档(2 个)
- 流程定义 get 接口 id 与 key 都为空时直接返回 null,避免 Flowable 抛 key is null
- /v3/api-docs 分组不存在时由 OpenApiResourceNotFoundException 兜底为 404

说明:member 模块 11 张表在库中均不存在,经确认前端未使用该模块,按要求不处理。
已添加2个文件
已修改13个文件
175 ■■■■■ 文件已修改
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/ListJsonStringConverter.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalTimeStringConverter.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/core/TranslateUtils.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/CrmContactController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/PermissionController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/ListJsonStringConverter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.framework.excel.core.convert;
import cn.hutool.json.JSONUtil;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import java.util.List;
/**
 * List å†™å‡ºä¸º JSON æ–‡æœ¬çš„转换器
 *
 * <p>FastExcel å†…置没有 List çš„转换器,导出含 List å­—段的 VO æ—¶
 * ä¼šæŠ› {@code Can not find 'Converter' support class List},接口直接 500。
 * æœ¬è½¬æ¢å™¨æŠŠé›†åˆåºåˆ—化成 JSON æ–‡æœ¬å†™å…¥å•元格,保证导出可用。</p>
 *
 * @author è¶…级管理员
 */
public class ListJsonStringConverter implements Converter<List> {
    @Override
    public Class<List> supportJavaTypeKey() {
        return List.class;
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }
    @Override
    public WriteCellData<?> convertToExcelData(List value, ExcelContentProperty contentProperty,
                                               GlobalConfiguration globalConfiguration) {
        return new WriteCellData<>(JSONUtil.toJsonStr(value));
    }
}
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalTimeStringConverter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.framework.excel.core.convert;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
 * LocalTime å†™å‡ºä¸ºçº¯æ–‡æœ¬ï¼ˆHH:mm:ss)的转换器
 *
 * <p>FastExcel å†…置没有 LocalTime çš„转换器,导出含 LocalTime å­—段的 VO æ—¶
 * ä¼šæŠ› {@code Can not find 'Converter' support class LocalTime},接口直接 500。
 * æ³¨å†Œæœ¬è½¬æ¢å™¨åŽå¯æ­£å¸¸å¯¼å‡ºã€‚</p>
 *
 * @author è¶…级管理员
 */
public class LocalTimeStringConverter implements Converter<LocalTime> {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
    @Override
    public Class<LocalTime> supportJavaTypeKey() {
        return LocalTime.class;
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }
    @Override
    public WriteCellData<?> convertToExcelData(LocalTime value, ExcelContentProperty contentProperty,
                                               GlobalConfiguration globalConfiguration) {
        return new WriteCellData<>(value.format(FORMATTER));
    }
}
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
@@ -10,6 +10,8 @@
import cn.idev.excel.exception.ExcelCommonException;
import cn.idev.excel.exception.ExcelDataConvertException;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.excel.core.convert.ListJsonStringConverter;
import cn.iocoder.yudao.framework.excel.core.convert.LocalTimeStringConverter;
import lombok.extern.slf4j.Slf4j;
import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
import cn.iocoder.yudao.framework.excel.core.handler.DateAsTextCellWriteHandler;
@@ -51,6 +53,8 @@
                .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // åŸºäºŽ column é•¿åº¦ï¼Œè‡ªåŠ¨é€‚é…ã€‚æœ€å¤§ 255 å®½åº¦
                .registerWriteHandler(new SelectSheetWriteHandler(head)) // åŸºäºŽå›ºå®š sheet å®žçŽ°ä¸‹æ‹‰æ¡†
                .registerConverter(new LongStringConverter()) // é¿å… Long ç±»åž‹ä¸¢å¤±ç²¾åº¦
                .registerConverter(new LocalTimeStringConverter()) // FastExcel å†…置无 LocalTime,缺少时会抛 Can not find 'Converter'
                .registerConverter(new ListJsonStringConverter()) // FastExcel å†…置无 List,缺少时会抛 Can not find 'Converter'
                .build();
        try {
            excelWriter.write(data, FastExcelFactory.writerSheet(sheetName).build());
@@ -82,6 +86,8 @@
                .registerWriteHandler(new SelectSheetWriteHandler(head))
                .registerWriteHandler(new DateAsTextCellWriteHandler())
                .registerConverter(new LongStringConverter())
                .registerConverter(new LocalTimeStringConverter())
                .registerConverter(new ListJsonStringConverter())
                .build();
        try {
            excelWriter.write(data, FastExcelFactory.writerSheet(sheetName).build());
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/core/TranslateUtils.java
@@ -28,7 +28,11 @@
     * @return ç¿»è¯‘结果
     */
    public static <T extends VO> List<T> translate(List<T> data) {
        if (CollUtil.isNotEmpty((data))) {
        // transService ç”± TranslateConfig åœ¨ easy-trans å¯ç”¨æ—¶æ³¨å…¥ï¼›
        // Spring Boot 4 ä¸‹ easy-trans è‡ªåŠ¨é…ç½®è¢«ç¦ç”¨ï¼ˆè§å„ profile çš„ spring.autoconfigure.exclude),
        // æ­¤æ—¶ transService ä¸º null。翻译只是增强展示,缺失时直接返回原数据,
        // ä¸èƒ½è®©å¯¼å‡ºç±»æŽ¥å£å› ä¸ºç¿»è¯‘组件不可用而抛 NPE å˜æˆ 500。
        if (transService != null && CollUtil.isNotEmpty((data))) {
            transService.transBatch(data);
        }
        return data;
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
@@ -39,6 +39,7 @@
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import org.springdoc.api.OpenApiResourceNotFoundException;
import tools.jackson.databind.exc.InvalidFormatException;
import java.time.LocalDateTime;
@@ -108,6 +109,11 @@
        if (ex instanceof NoResourceFoundException) {
            return noResourceFoundExceptionHandler(request, (NoResourceFoundException) ex);
        }
        // /v3/api-docs/{group} ä¼ å…¥ä¸å­˜åœ¨çš„分组:属于资源不存在,应返回 404 è€Œä¸æ˜¯ 500
        if (ex instanceof OpenApiResourceNotFoundException) {
            log.warn("[openApiResourceNotFoundExceptionHandler][{}]", ex.getMessage());
            return CommonResult.error(NOT_FOUND.getCode(), "接口分组不存在: " + ex.getMessage());
        }
        if (ex instanceof HttpRequestMethodNotSupportedException) {
            return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
        }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
@@ -117,7 +118,13 @@
    public CommonResult<BpmProcessDefinitionRespVO> getProcessDefinition(
            @RequestParam(value = "id", required = false) String id,
            @RequestParam(value = "key", required = false) String key) {
        ProcessDefinition processDefinition = id != null ? processDefinitionService.getProcessDefinition(id)
        // id ä¸Ž key éƒ½ä¸ºç©ºæ—¶ç›´æŽ¥è¿”回 null:否则会调用 getActiveProcessDefinition(null),
        // ç”± Flowable æŠ› "key is null" çš„ IllegalArgumentException å˜æˆ 500
        if (StrUtil.isBlank(id) && StrUtil.isBlank(key)) {
            return success(null);
        }
        ProcessDefinition processDefinition = StrUtil.isNotBlank(id)
                ? processDefinitionService.getProcessDefinition(id)
                : processDefinitionService.getActiveProcessDefinition(key);
        if (processDefinition == null) {
            return success(null);
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/CrmContactController.java
@@ -40,6 +40,8 @@
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
@@ -133,7 +135,9 @@
    @GetMapping("/page-by-customer")
    @Operation(summary = "获得联系人分页,基于指定客户")
    public CommonResult<PageResult<CrmContactRespVO>> getContactPageByCustomer(@Valid CrmContactPageReqVO pageVO) {
        Assert.notNull(pageVO.getCustomerId(), "客户编号不能为空");
        if (pageVO.getCustomerId() == null) {
            throw exception0(BAD_REQUEST.getCode(), "客户编号不能为空");
        }
        PageResult<CrmContactDO> pageResult = contactService.getContactPageByCustomerId(pageVO);
        return success(new PageResult<>(buildContactDetailList(pageResult.getList()), pageResult.getTotal()));
    }
@@ -141,7 +145,9 @@
    @GetMapping("/page-by-business")
    @Operation(summary = "获得联系人分页,基于指定商机")
    public CommonResult<PageResult<CrmContactRespVO>> getContactPageByBusiness(@Valid CrmContactPageReqVO pageVO) {
        Assert.notNull(pageVO.getBusinessId(), "商机编号不能为空");
        if (pageVO.getBusinessId() == null) {
            throw exception0(BAD_REQUEST.getCode(), "商机编号不能为空");
        }
        PageResult<CrmContactDO> pageResult = contactService.getContactPageByBusinessId(pageVO);
        return success(new PageResult<>(buildContactDetailList(pageResult.getList()), pageResult.getTotal()));
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
@@ -58,6 +58,8 @@
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -150,7 +152,9 @@
    @GetMapping("/page-by-customer")
    @Operation(summary = "获得合同分页,基于指定客户")
    public CommonResult<PageResult<CrmContractRespVO>> getContractPageByCustomer(@Valid CrmContractPageReqVO pageVO) {
        Assert.notNull(pageVO.getCustomerId(), "客户编号不能为空");
        if (pageVO.getCustomerId() == null) {
            throw exception0(BAD_REQUEST.getCode(), "客户编号不能为空");
        }
        PageResult<CrmContractDO> pageResult = contractService.getContractPageByCustomerId(pageVO);
        return success(BeanUtils.toBean(pageResult, CrmContractRespVO.class).setList(buildContractDetailList(pageResult.getList())));
    }
@@ -158,7 +162,9 @@
    @GetMapping("/page-by-business")
    @Operation(summary = "获得合同分页,基于指定商机")
    public CommonResult<PageResult<CrmContractRespVO>> getContractPageByBusiness(@Valid CrmContractPageReqVO pageVO) {
        Assert.notNull(pageVO.getBusinessId(), "商机编号不能为空");
        if (pageVO.getBusinessId() == null) {
            throw exception0(BAD_REQUEST.getCode(), "商机编号不能为空");
        }
        PageResult<CrmContractDO> pageResult = contractService.getContractPageByBusinessId(pageVO);
        return success(BeanUtils.toBean(pageResult, CrmContractRespVO.class).setList(buildContractDetailList(pageResult.getList())));
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/invoice/CrmInvoiceController.java
@@ -49,6 +49,8 @@
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -126,7 +128,9 @@
    @GetMapping("/page-by-customer")
    @Operation(summary = "获得开票分页,基于指定客户")
    public CommonResult<PageResult<CrmInvoiceRespVO>> getInvoicePageByCustomer(@Valid CrmInvoicePageReqVO pageReqVO) {
        Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
        if (pageReqVO.getCustomerId() == null) {
            throw exception0(BAD_REQUEST.getCode(), "客户编号不能为空");
        }
        PageResult<CrmInvoiceDO> pageResult = invoiceService.getInvoicePageByCustomerId(pageReqVO);
        return success(new PageResult<>(buildInvoiceDetailList(pageResult.getList()), pageResult.getTotal()));
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivableController.java
@@ -44,6 +44,8 @@
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -119,7 +121,9 @@
    @GetMapping("/page-by-customer")
    @Operation(summary = "获得回款分页,基于指定客户")
    public CommonResult<PageResult<CrmReceivableRespVO>> getReceivablePageByCustomer(@Valid CrmReceivablePageReqVO pageReqVO) {
        Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
        if (pageReqVO.getCustomerId() == null) {
            throw exception0(BAD_REQUEST.getCode(), "客户编号不能为空");
        }
        PageResult<CrmReceivableDO> pageResult = receivableService.getReceivablePageByCustomerId(pageReqVO);
        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/CrmReceivablePlanController.java
@@ -41,6 +41,8 @@
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
@@ -113,7 +115,9 @@
    @GetMapping("/page-by-customer")
    @Operation(summary = "获得回款计划分页,基于指定客户")
    public CommonResult<PageResult<CrmReceivablePlanRespVO>> getReceivablePlanPageByCustomer(@Valid CrmReceivablePlanPageReqVO pageReqVO) {
        Assert.notNull(pageReqVO.getCustomerId(), "客户编号不能为空");
        if (pageReqVO.getCustomerId() == null) {
            throw exception0(BAD_REQUEST.getCode(), "客户编号不能为空");
        }
        PageResult<CrmReceivablePlanDO> pageResult = receivablePlanService.getReceivablePlanPageByCustomerId(pageReqVO);
        return success(new PageResult<>(buildReceivableDetailList(pageResult.getList()), pageResult.getTotal()));
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractMapper.java
@@ -164,7 +164,9 @@
        LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now());
        if (CrmContractPageReqVO.EXPIRY_TYPE_ABOUT_TO_EXPIRE.equals(pageReqVO.getExpiryType())) { // å³å°†åˆ°æœŸ
            query.eq(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.APPROVE.getStatus())
                    .between(CrmContractDO::getEndTime, beginOfToday, endOfToday.plusDays(config.getNotifyDays()));
                    // notifyDays å¯èƒ½ä¸º null(历史配置未填),为 null æ—¶æŒ‰ 0 å¤©å¤„理,避免拆箱 NPE
                .between(CrmContractDO::getEndTime, beginOfToday,
                        endOfToday.plusDays(config.getNotifyDays() != null ? config.getNotifyDays() : 0));
        } else if (CrmContractPageReqVO.EXPIRY_TYPE_EXPIRED.equals(pageReqVO.getExpiryType())) { // å·²åˆ°æœŸ
            query.eq(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.APPROVE.getStatus())
                    .lt(CrmContractDO::getEndTime, endOfToday);
@@ -199,7 +201,9 @@
        LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
        LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now());
        query.eq(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.APPROVE.getStatus()) // å¿…须审批通过!
                .between(CrmContractDO::getEndTime, beginOfToday, endOfToday.plusDays(config.getNotifyDays()));
                // notifyDays å¯èƒ½ä¸º null(历史配置未填),为 null æ—¶æŒ‰ 0 å¤©å¤„理,避免拆箱 NPE
                .between(CrmContractDO::getEndTime, beginOfToday,
                        endOfToday.plusDays(config.getNotifyDays() != null ? config.getNotifyDays() : 0));
        return selectCount(query);
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -530,9 +530,19 @@
        if (config == null || Boolean.FALSE.equals(config.getNotifyEnabled())) {
            return 0L;
        }
        // æé†’天数未配置时按默认 7 å¤©å¤„理,避免 config.getNotifyDays() ä¸º null æ—¶æ‹†ç®±æŠ› NPE å¯¼è‡´æŽ¥å£ 500
        if (config.getNotifyDays() == null) {
            config = BeanUtils.toBean(config, CrmContractConfigDO.class);
            config.setNotifyDays(DEFAULT_NOTIFY_DAYS);
        }
        return contractMapper.selectCountByRemind(userId, config);
    }
    /**
     * åˆåŒåˆ°æœŸæé†’的默认提前天数:合同提醒未配置 notifyDays æ—¶ä½¿ç”¨
     */
    private static final Integer DEFAULT_NOTIFY_DAYS = 7;
    @Override
    public List<CrmContractDO> getContractListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId) {
        return contractMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java
@@ -27,6 +27,8 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP;
@@ -67,8 +69,9 @@
        } else if (groupId != null && userId != null) {
            member = groupMemberService.getGroupMember(groupId, userId);
        } else {
            // é¿å… selectByGroupIdAndUserId æ”¶åˆ° null å‚数走全表扫 / æŠ› SQL å¼‚常
            throw new IllegalArgumentException("参数缺失:需传 id æˆ– (groupId, userId)");
            // é¿å… selectByGroupIdAndUserId æ”¶åˆ° null å‚数走全表扫 / æŠ› SQL å¼‚常。
            // è¿™é‡Œå±žäºŽå®¢æˆ·ç«¯å‚数问题,抛业务异常返回 400,不要用 IllegalArgumentException å˜æˆ 500
            throw exception0(BAD_REQUEST.getCode(), "参数缺失:需传 id æˆ– (groupId, userId)");
        }
        if (member == null) {
            return success(null);
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/PermissionController.java
@@ -34,7 +34,10 @@
    @Parameter(name = "roleId", description = "角色编号", required = true)
    @GetMapping("/list-role-menus")
    @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')")
    public CommonResult<Set<Long>> getRoleMenuList(Long roleId) {
    public CommonResult<Set<Long>> getRoleMenuList(@RequestParam("roleId") Long roleId) {
        // æ˜¾å¼å£°æ˜Žå¿…填:roleId ç¼ºå¤±æ—¶ç”± Spring è¿”回 400「请求参数不正确」,
        // è€Œä¸æ˜¯ä¼ åˆ° RoleServiceImpl ç”¨ null ä½œä¸ºç¼“å­˜ key,抛
        // IllegalArgumentException: Null key returned for cache operation å˜æˆ 500
        return success(permissionService.getRoleMenuListByRoleId(roleId));
    }