自定义复合注解|切面|反射|策略模式校验入参 a表中x字段单价之和是否大于b表中的y字段总金额> ,大于就抛异常,使用了策略模式后面可以增加校验类型:加减乘除等类型的校验,使用策略模式进行选举那个策略,只用自定义注进行解耦与切面进行方法执行前拦截

  1. 基础配置-工具类|异常|字典
    0.1. 通过反射获取要比较的字段值
/**
 * 比较的工具类
 *
 * @Author LiZhiMin
 * @Date 2024/9/3 10:59
 */

public class CompareUtil {
    /**
     * 获取指定对象的字段值。
     * <p>
     * 根据给定的字段名称,从对象中提取字段值。
     * </p>
     *
     * @param clazz     对象的类类型。
     * @param instance  对象实例。
     * @param fieldName 字段名称。
     * @return 字段的值。
     * @throws NoSuchFieldException   如果指定字段不存在。
     * @throws IllegalAccessException 如果无法访问字段。
     */
    public static BigDecimal getFieldValue(Class<?> clazz, Object instance, String fieldName) throws NoSuchFieldException, IllegalAccessException {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return (BigDecimal) field.get(instance);
    }
}

0.2.异常的常量

/**
 * scrm 错误码枚举类
 *
 * scrm 系统,使用 1_099_000_000 段
 * @Author LiZhiMin
 * @Date 2024/5/15 18:23
 */

public interface ErrorCodeConstants {
    //比较报错信息
    ErrorCode COMPARE_ORDER_RECEIVABLE = new ErrorCode(2_099_666_001, "已经存在的回款金额:【{}】 + 你填写的金额:【{}】 = 【{}】,不能大于订单总金额:【{}】,你最大能填写的金额为:【{}】");

    ErrorCode COMPARE_ORDER_INVOICES = new ErrorCode(2_099_666_002, "已经存在的发票金额:【{}】 + 你填写的金额:【{}】= 【{}】,不能大于订单总金额:【{}】,你最大能填写的金额为:【{}】");

    ErrorCode COMPARE_STRATEGY_TYPE = new ErrorCode(2_099_666_003, "找不到这个策略类型:{}");

    ErrorCode COMPARE_STRATEGY_TYPE_NOT_NULL = new ErrorCode(2_099_666_004, "策略类型不能为空,请联系管理员");


}

0.3.字典类

/**
 * CRM 业务类型枚举
 *
 * @author HUIHUI
 */
@RequiredArgsConstructor
@Getter
public enum CrmBizTypeEnum implements IntArrayValuable {

    CRM_CLUE(1, "线索"),
    CRM_CUSTOMER(2, "客户"),
    CRM_CONTACT(3, "联系人"),
    CRM_BUSINESS(4, "商机"),
    CRM_CONTRACT(5, "合同"),
    CRM_PRODUCT(6, "产品"),
    CRM_RECEIVABLE(7, "回款"),
    CRM_RECEIVABLE_PLAN(8, "回款计划"),
    CRM_ORDER(9, "订单"),
    CRM_INVOICE(10, "发票"),
    CRM_REFUND(11, "退款"),
    ;

    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CrmBizTypeEnum::getType).toArray();

    /**
     * 类型
     */
    private final Integer type;
    /**
     * 名称
     */
    private final String name;

    public static String getNameByType(Integer type) {
        CrmBizTypeEnum typeEnum = CollUtil.findOne(CollUtil.newArrayList(CrmBizTypeEnum.values()),
                item -> ObjUtil.equal(item.type, type));
        return typeEnum == null ? null : typeEnum.getName();
    }

    @Override
    public int[] array() {
        return ARRAYS;
    }

}

0.4、异常工具类

/**
 * {@link ServiceException} 工具类
 *
 * 目的在于,格式化异常信息提示。
 * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
 *
 * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式:
 *
 * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration
 * 2. 异常提示信息,写在 .properties 等等配置文件
 * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新
 * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新
 */
@Slf4j
public class ServiceExceptionUtil {

    /**
     * 错误码提示模板
     */
    private static final ConcurrentMap<Integer, String> MESSAGES = new ConcurrentHashMap<>();

    public static void putAll(Map<Integer, String> messages) {
        ServiceExceptionUtil.MESSAGES.putAll(messages);
    }

    public static void put(Integer code, String message) {
        ServiceExceptionUtil.MESSAGES.put(code, message);
    }

    public static void delete(Integer code, String message) {
        ServiceExceptionUtil.MESSAGES.remove(code, message);
    }

    // ========== 和 ServiceException 的集成 ==========

    public static ServiceException exception(ErrorCode errorCode) {
        String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
        return exception0(errorCode.getCode(), messagePattern);
    }

    public static ServiceException exception(ErrorCode errorCode, Object... params) {
        String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
        return exception0(errorCode.getCode(), messagePattern, params);
    }

    /**
     * 创建指定编号的 ServiceException 的异常
     *
     * @param code 编号
     * @return 异常
     */
    public static ServiceException exception(Integer code) {
        return exception0(code, MESSAGES.get(code));
    }

    /**
     * 创建指定编号的 ServiceException 的异常
     *
     * @param code 编号
     * @param params 消息提示的占位符对应的参数
     * @return 异常
     */
    public static ServiceException exception(Integer code, Object... params) {
        return exception0(code, MESSAGES.get(code), params);
    }

    public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
        String message = doFormat(code, messagePattern, params);
        return new ServiceException(code, message);
    }

    public static ServiceException invalidParamException(String messagePattern, Object... params) {
        return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params);
    }

    // ========== 格式化方法 ==========

    /**
     * 将错误编号对应的消息使用 params 进行格式化。
     *
     * @param code           错误编号
     * @param messagePattern 消息模版
     * @param params         参数
     * @return 格式化后的提示
     */
    @VisibleForTesting
    public static String doFormat(int code, String messagePattern, Object... params) {
        StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
        int i = 0;
        int j;
        int l;
        for (l = 0; l < params.length; l++) {
            j = messagePattern.indexOf("{}", i);
            if (j == -1) {
                log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
                if (i == 0) {
                    return messagePattern;
                } else {
                    sbuf.append(messagePattern.substring(i));
                    return sbuf.toString();
                }
            } else {
                sbuf.append(messagePattern, i, j);
                sbuf.append(params[l]);
                i = j + 2;
            }
        }
        if (messagePattern.indexOf("{}", i) != -1) {
            log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
        }
        sbuf.append(messagePattern.substring(i));
        return sbuf.toString();
    }

}

0.5、错误码对象

/**
 * 错误码对象
 *
 * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
 * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
 *
 * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
 */
@Data
public class ErrorCode {

    /**
     * 错误码
     */
    private final Integer code;
    /**
     * 错误提示
     */
    private final String msg;

    public ErrorCode(Integer code, String message) {
        this.code = code;
        this.msg = message;
    }

}

1.定义复合注解

/**
 * 自定义注解类处理器复合注解
 * 比较a表中x字段值是否大于b表中y字段值
 *
 * @Author LiZhiMin
 * @Date 2024/8/31 17:34
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldCompareValidators {

    /**
     * 要查询的主表的类型 {@link CrmBizTypeEnum#getType()}
     */
    CrmBizTypeEnum foreignKeyType() default CrmBizTypeEnum.CRM_RECEIVABLE;

    /**
     * 要查询的主表的字段名字
     */
    FieldNameType foreignKeyFieldName() default FieldNameType.SCRM_PRICE;


    //要校验的注解多个
    FieldCompareValidator[] fieldCompareValidator();

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

1.1.定义子注解

/**
 * 自定义注解类处理器
 * 比较a表中x字段值是否大于b表中y字段值
 *
 * @Author LiZhiMin
 * @Date 2024/8/21 12:18
 */

@Retention(RetentionPolicy.RUNTIME)// 指定注解的生命周期是运行时
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})// 指定注解可以使用的位置
public @interface FieldCompareValidator {

    /**
     * 默认错误提示信息
     */
    String message() default "金额不能大于订单总金额!";


    /**
     * 要查询的外键表的类型  {@link CrmBizTypeEnum#getType()}
     */
    CrmBizTypeEnum majorJeyType() default CrmBizTypeEnum.CRM_ORDER;

    /**
     * 要查询的外键表的字段名字
     */
    FieldNameType majorJeyFieldName() default FieldNameType.SCRM_ORDER_TOTAL_MONEY;


}

1.2.使用复合注解在接口上面

@PostMapping("/create")
    @Operation(summary = "创建发票")
    @PreAuthorize("@ss.hasPermission('scrm:invoice:create')")
    @FieldCompareValidators( foreignKeyType = CrmBizTypeEnum.CRM_INVOICE,foreignKeyFieldName = FieldNameType.SCRM_PRICE,fieldCompareValidator = {
            @FieldCompareValidator(majorJeyType = CrmBizTypeEnum.CRM_ORDER,majorJeyFieldName = FieldNameType.SCRM_ORDER_TOTAL_MONEY,message = "金额限制")
    })
    public CommonResult<Long> createInvoice(@Valid @RequestBody InvoiceSaveReqVO createReqVO) {
        return success(invoiceService.createInvoice(createReqVO));
    }

2.定义一个切面在切面中拦截注解在方法执行之前执行,并且编写功能

/**
 * 处理字段比较验证的切面类。
 * <p>
 * 该类是一个切面类,用于在方法执行之前,基于自定义注解 {@link FieldCompareValidators} 提供的配置信息,
 * 执行字段比较验证逻辑。通过切面技术,实现了在运行时动态地插入验证逻辑,从而增强系统的灵活性和可维护性。
 * </p>
 *
 * @Author LiZhiMin
 * @Date 2024/8/31 17:38
 */
@Aspect
@Component
@Slf4j
public class FieldCompareValidatorAspect {

    @Autowired
    private CompareStrategyFactory compareStrategyFactory;

    /**
     * 切点方法,用于在被 {@link FieldCompareValidators} 注解标记的方法执行之前触发。
     * <p>
     * 从注解中提取字段比较配置,并根据这些配置选择并执行相应的比较策略。
     * 该方法获取方法参数,提取比较需要的字段信息,并通过 {@link CompareStrategyFactory} 获取对应的比较策略进行验证。
     * </p>
     *
     * @param joinPoint              连接点,提供了方法执行的信息和上下文。
     * @param fieldCompareValidators 自定义注解 {@link FieldCompareValidators},提供字段比较的配置信息。
     */
    @Before("@annotation(fieldCompareValidators)")
    public void pointcut(JoinPoint joinPoint, FieldCompareValidators fieldCompareValidators) {
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        // 从注解中提取主表枚举类型
        CrmBizTypeEnum crmBizTypeEnumForeignKey = fieldCompareValidators.foreignKeyType();
        // 获取要查询的主表字段名
        String foreignKeyFieldName = fieldCompareValidators.foreignKeyFieldName().getFieldName();

        // 遍历所有字段比较验证配置
        for (FieldCompareValidator compareValidator : fieldCompareValidators.fieldCompareValidator()) {
            // 获取比较字段名和关联表枚举类型
            String majorKeyFieldName = compareValidator.majorJeyFieldName().getFieldName();
            CrmBizTypeEnum crmBizTypeEnumMajorKey = compareValidator.majorJeyType();

            // 从工厂获取对应的比较策略
            CompareStrategy strategy = compareStrategyFactory.getStrategy(crmBizTypeEnumForeignKey, crmBizTypeEnumMajorKey);

            if (strategy != null) {
                // 执行策略进行字段比较验证
                strategy.handle(args[0], foreignKeyFieldName, majorKeyFieldName);
            } else {
                // 如果未找到对应的策略,则抛出异常
                throw exception(COMPARE_STRATEGY_TYPE_NOT_NULL);
            }
        }
    }
}

3.定义一个策略接口

/**
 * 比较策略接口,定义了所有比较策略的通用行为。
 * <p>
 * 实现这个接口的策略类将具体定义如何处理给定的请求对象。
 * 未来可以扩展该接口,以支持更多类型的运算,如加减乘除等..。
 * </p>
 *
 * @Author LiZhiMin
 * @Date 2024/9/3 10:44
 */

public interface CompareStrategy {

    /**
     * 处理比较策略的核心方法。
     * <p>
     * 实现该方法的策略类需要根据具体的业务逻辑来处理请求对象。
     * </p>
     *
     * @param request             请求对象,通常是需要进行比较或校验的数据。
     * @param foreignKeyFieldName 外部业务类型字段名称,用于标识外部系统的数据字段。
     * @param majorKeyFieldName   主要业务类型字段名称,用于标识主要系统的数据字段。
     */
    void handle(Object request, String foreignKeyFieldName, String majorKeyFieldName);
}

5.实现策略类实现不同类型对应不同功能
5.1.策略实现方式一

/**
 * 实现了 CompareStrategy 接口,用于处理订单和发票金额比较的逻辑。
 * 主要功能包括验证发票总金额是否超过订单金额,以及计算现有发票总金额。
 *
 * @Author LiZhiMin
 * @Date 2024/9/3 12:15
 */
@Component
public class OrderInvoiceStrategyImpl implements CompareStrategy {
    @Resource
    private InvoiceMapper invoiceMapper;

    @Resource
    private OrderMapper orderMapper;

    /**
     * 处理 {@link InvoiceSaveReqVO} 类型的数据。
     * <p>
     * 获取订单信息、计算主键字段的值和外表字段的值,比较主表已存在的应收金额与新增的应收金额是否符合预期。
     * </p>
     *
     * @param request             请求对象,必须是 {@link InvoiceSaveReqVO} 类型。
     * @param foreignKeyFieldName 外部业务类型字段名称,用于标识应收表中的字段。
     * @param majorKeyFieldName   主要业务类型字段名称,用于标识订单表中的字段。
     */
    @Override
    public void handle(Object request, String foreignKeyFieldName, String majorKeyFieldName) {
        if (request instanceof InvoiceSaveReqVO) {
            // 处理 ReceivableSaveReqVO 类型的数据
            processSaveReqVoByType(request, foreignKeyFieldName, majorKeyFieldName);
        } else {
            throw exception(COMPARE_STRATEGY_TYPE, request.getClass().getName());
        }
    }

    /**
     * 处理 InvoiceSaveReqVO 类型的数据,进行金额比较和验证。
     *
     * @param objVo               请求对象,应为 InvoiceSaveReqVO 类型
     * @param foreignKeyFieldName 发票对象中的字段名
     * @param majorJeyFieldName   订单对象中的主要字段名
     */
    private void processSaveReqVoByType(Object objVo, String foreignKeyFieldName, String majorJeyFieldName) {
        try {
            InvoiceSaveReqVO vo = (InvoiceSaveReqVO) objVo;
            // 获取订单信息
            OrderDO orderDO = orderMapper.selectById(vo.getOrderId());

            // 计算主键字段的值
            BigDecimal foreignKeyFieldValue = getFieldValue(InvoiceSaveReqVO.class, vo, foreignKeyFieldName);

            // 计算外表字段的值-总金额
            BigDecimal totalMoneyMajorKeyFieldValue = getFieldValue(OrderDO.class, orderDO, majorJeyFieldName);

            // 计算主表已存在的应收金额
            BigDecimal totalPriceSum = getTotalInvoicePrice(vo.getOrderId(), foreignKeyFieldName);

            // 加上新增的应收金额
            BigDecimal totalPrice = totalPriceSum.add(foreignKeyFieldValue);

            // 比较总金额与订单金额,如果超出则抛出异常
            if (totalPrice.compareTo(totalMoneyMajorKeyFieldValue) > 0) {
                throw exception(COMPARE_ORDER_INVOICES, totalPriceSum, foreignKeyFieldValue, totalPrice, totalMoneyMajorKeyFieldValue, totalMoneyMajorKeyFieldValue.subtract(totalPriceSum));
            }

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 计算指定订单的总发票金额。
     *
     * @param orderId             订单 ID
     * @param foreignKeyFieldName 外键字段名
     * @return 总发票金额
     */
    private BigDecimal getTotalInvoicePrice(Long orderId, String foreignKeyFieldName) {
        List<InvoiceDO> list = invoiceMapper.selectList(InvoiceDO::getOrderId, orderId);
        return list.stream().map(invoiceDO -> {
            try {
                return getFieldValue(InvoiceDO.class, invoiceDO, foreignKeyFieldName);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
                return BigDecimal.ZERO; // 返回零值以避免异常中断流
            }
        }).reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

5.1.策略实现方式二

/**
 * 处理订单应收金额比较策略的实现类。
 * <p>
 * 该类实现了 {@link CompareStrategy} 接口,专门用于处理与应收金额相关的比较逻辑。
 * 在处理请求时,会检查请求对象的类型,并根据业务逻辑计算应收金额是否符合预期。
 * </p>
 *
 * @Author LiZhiMin
 * @Date 2024/9/3 10:47
 */
@Component
public class OrderReceivableStrategyImpl implements CompareStrategy {
    @Resource
    private ReceivableMapper receivableMapper;

    @Resource
    private OrderMapper orderMapper;

    /**
     * 处理比较策略的核心方法。
     * <p>
     * 检查请求对象的类型是否为 {@link ReceivableSaveReqVO},并将其委托给相应的方法进行处理。
     * 如果请求对象类型不符合要求,则抛出异常。
     * </p>
     *
     * @param request             请求对象,通常是 {@link ReceivableSaveReqVO} 类型的数据。
     * @param foreignKeyFieldName 外部业务类型字段名称,用于标识应收表中的字段。
     * @param majorKeyFieldName   主要业务类型字段名称,用于标识订单表中的字段。
     */
    @Override
    public void handle(Object request, String foreignKeyFieldName, String majorKeyFieldName) {
        if (request instanceof ReceivableSaveReqVO) {
            // 处理 ReceivableSaveReqVO 类型的数据
            processSaveReqVoByType(request, foreignKeyFieldName, majorKeyFieldName);
        } else {
            throw exception(COMPARE_STRATEGY_TYPE, request.getClass().getName());
        }
    }

    /**
     * 处理 {@link ReceivableSaveReqVO} 类型的数据。
     * <p>
     * 获取订单信息、计算主键字段的值和外表字段的值,比较主表已存在的应收金额与新增的应收金额是否符合预期。
     * </p>
     *
     * @param objVo               请求对象,必须是 {@link ReceivableSaveReqVO} 类型。
     * @param foreignKeyFieldName 外部业务类型字段名称,用于标识应收表中的字段。
     * @param majorJeyFieldName   主要业务类型字段名称,用于标识订单表中的字段。
     */
    private void processSaveReqVoByType(Object objVo, String foreignKeyFieldName, String majorJeyFieldName) {
        try {
            ReceivableSaveReqVO vo = (ReceivableSaveReqVO) objVo;
            // 获取订单信息
            OrderDO orderDO = orderMapper.selectById(vo.getOrderId());

            // 计算主键字段的值
            BigDecimal foreignKeyFieldValue = getFieldValue(ReceivableSaveReqVO.class, vo, foreignKeyFieldName);

            // 计算外表字段的值-总金额
            BigDecimal totalMoneyMajorKeyFieldValue = getFieldValue(OrderDO.class, orderDO, majorJeyFieldName);

            // 计算主表已存在的应收金额
            BigDecimal totalPriceSum = getTotalReceivablePrice(vo.getOrderId(), foreignKeyFieldName);

            // 加上新增的应收金额
            BigDecimal totalPrice = totalPriceSum.add(foreignKeyFieldValue);

            // 比较金额,若总金额大于订单金额,则抛出异常
            if (totalPrice.compareTo(totalMoneyMajorKeyFieldValue) > 0) {
                throw exception(COMPARE_ORDER_RECEIVABLE, totalPriceSum, foreignKeyFieldValue, totalPrice, totalMoneyMajorKeyFieldValue, totalMoneyMajorKeyFieldValue.subtract(totalPriceSum));
            }

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 计算主表总的应收金额。
     * <p>
     * 查询所有与给定订单 ID 关联的应收记录,并计算所有记录中指定字段的总金额。
     * </p>
     *
     * @param orderId             订单 ID,用于查询应收记录。
     * @param foreignKeyFieldName 外部业务类型字段名称,用于标识应收表中的字段。
     * @return 主表总的应收金额。
     */
    private BigDecimal getTotalReceivablePrice(Long orderId, String foreignKeyFieldName) {
        List<ReceivableDO> list = receivableMapper.selectList(ReceivableDO::getOrderId, orderId);
        return list.stream().map(receivableDO -> {
            try {
                return getFieldValue(ReceivableDO.class, receivableDO, foreignKeyFieldName);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
                return BigDecimal.ZERO; // 返回零值以避免异常中断流
            }
        }).reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

6、定义策略工厂类编写方法在程序启动时注入不同的策略

/**
 * @Author LiZhiMin
 * 比较策略工厂类,用于管理和检索不同的 {@link CompareStrategy} 实例。
 * <p>
 * 该类作为一个策略注册中心,将不同的比较策略根据业务类型对进行存储和管理。
 * 通过这个工厂类,可以根据业务类型对获取对应的比较策略实例。
 * </p>
 * @Date 2024/9/3 10:50
 */
@Component
public class CompareStrategyFactory {

    /**
     * 存储比较策略的映射表。
     * <p>
     * 这个映射表的键是一个 {@link Pair} 对象,表示主要业务类型和外部业务类型的组合。
     * 值是对应的 {@link CompareStrategy} 实例。
     * </p>
     */
    private final Map<Pair<CrmBizTypeEnum, CrmBizTypeEnum>, CompareStrategy> strategyFactory = new HashMap<>();

    /**
     * 构造函数,初始化工厂并注册策略。
     * <p>
     * 通过传入的策略列表,遍历每一个策略实例,并将其注册到工厂中。
     * 策略的注册是基于其具体实现类型来进行的。
     * </p>
     *
     * @param strategyList 需要注册的 {@link CompareStrategy} 实例列表。
     */
    @Autowired
    public CompareStrategyFactory(List<CompareStrategy> strategyList) {
        for (CompareStrategy strategy : strategyList) {
            // 将处理器注册到工厂中(可以根据需要调整键的类型)
            if (strategy instanceof OrderReceivableStrategyImpl) {
                strategyFactory.put(Pair.of(CrmBizTypeEnum.CRM_RECEIVABLE, CrmBizTypeEnum.CRM_ORDER), strategy);
            } else if (strategy instanceof OrderInvoiceStrategyImpl) {
                strategyFactory.put(Pair.of(CrmBizTypeEnum.CRM_INVOICE, CrmBizTypeEnum.CRM_ORDER), strategy);
            }
        }
    }

    /**
     * 根据主要业务类型和外部业务类型获取对应的比较策略实例。
     * <p>
     * 根据提供的主要业务类型和外部业务类型从工厂中查找并返回相应的 {@link CompareStrategy} 实例。
     * </p>
     *
     * @param majorKey   主要业务类型。
     * @param foreignKey 外部业务类型。
     * @return 对应的 {@link CompareStrategy} 实例,如果未找到则返回 {@code null}。
     */
    public CompareStrategy getStrategy(CrmBizTypeEnum majorKey, CrmBizTypeEnum foreignKey) {
        return strategyFactory.get(Pair.of(majorKey, foreignKey));
    }
}