自定義註解|切面|反射|策略模式進行入參比較校驗

泪伤荡發表於2024-09-03

自定義複合註解|切面|反射|策略模式校驗入參 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;
    }

}

0.6、全域性錯誤碼列舉

**
 * 全域性錯誤碼列舉
 * 0-999 系統異常編碼保留
 *
 * 一般情況下,使用 HTTP 響應狀態碼 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
 * 雖然說,HTTP 響應狀態碼作為業務使用表達能力偏弱,但是使用在系統層面還是非常不錯的
 * 比較特殊的是,因為之前一直使用 0 作為成功,就不使用 200 啦。
 *
 * @author 芋道原始碼
 */
public interface GlobalErrorCodeConstants {

    ErrorCode SUCCESS = new ErrorCode(0, "成功");

    // ========== 客戶端錯誤段 ==========

    ErrorCode BAD_REQUEST = new ErrorCode(400, "請求引數不正確");
    ErrorCode UNAUTHORIZED = new ErrorCode(401, "賬號未登入");
    ErrorCode FORBIDDEN = new ErrorCode(403, "沒有該操作許可權");
    ErrorCode NOT_FOUND = new ErrorCode(404, "請求未找到");
    ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "請求方法不正確");
    ErrorCode LOCKED = new ErrorCode(423, "請求失敗,請稍後重試"); // 併發請求,不允許
    ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "請求過於頻繁,請稍後重試");

    // ========== 服務端錯誤段 ==========

    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系統異常");
    ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未實現/未開啟");
    ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "錯誤的配置項");

    // ========== 自定義錯誤段 ==========
    ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重複請求,請稍後重試"); // 重複請求
    ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止寫操作");

    ErrorCode UNKNOWN = new ErrorCode(999, "未知錯誤");

}

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);
}

4.實現策略類實現不同型別對應不同功能
4.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);
    }
}

4.2.策略實現方式二

/**
 * 處理訂單應收金額比較策略的實現類。
 * <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);
    }
}

<h 5、定義策略工廠類編寫方法在程式啟動時注入不同的策略

/**
 * @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));
    }
}

6.使用注意事項
6.1.註解使用注意事項

6.2.具體策略類中的mapper層換成自己的

6.3.策略工廠以及策略類與spring管理bean的注意事項

6.4.如果不適用芋道的springcloud框架,那專案得繼承mabatis-plu或者將查詢資料的方法改為自己專案使用的

7.編寫時參考的部落格
7.1.策略模式
https://www.cnblogs.com/satire/p/14620980.html
7.2.反射
https://juejin.cn/post/6987191491638591502
7.3.自定義複合註解
https://blog.csdn.net/qq_41378597/article/details/102934784
7.4.切面
https://www.cnblogs.com/wzh2010/p/15886644.html

相關文章