国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

spring自定義注解+aop+@BindingParam

這篇具有很好參考價(jià)值的文章主要介紹了spring自定義注解+aop+@BindingParam。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

1.pom 引入

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2.聲明自定義注解

2.1 聲明切面注解

import java.lang.annotation.*;

/**
 * @author WANGCHENG
 * @version 1.0
 * @Description: 校驗(yàn)組合編輯權(quán)限注解
 * @date 2023/08/04 20:12
 */
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckInstructionAuth {
    /**
     * 指令類型  默認(rèn):銷售
     */
    CheckInstructionEditTypeEnum instructionType()
            default CheckInstructionEditTypeEnum.SALE;

    /**
     * 操作類型,必選,用來錯誤提示。
     * eg:選擇修改,校驗(yàn)提示:沒有XX組合的操作權(quán)限,不允許修改
     *
     * @return
     */
    CheckInstructionEditTypeEnum.OperateTypeEnum operateType();

    /**
     * 校驗(yàn)數(shù)據(jù)類型   默認(rèn):當(dāng)前數(shù)據(jù)
     */
    CheckInstructionEditTypeEnum.CheckDataTypeEnum checkDataType()
            default CheckInstructionEditTypeEnum.CheckDataTypeEnum.CURRENT;

    /**
     * 獲取數(shù)據(jù)方式,默認(rèn):@BindingParam 標(biāo)注
     */
    CheckInstructionEditTypeEnum.DataSourceEnum dataSource()
            default CheckInstructionEditTypeEnum.DataSourceEnum.ANNOTATION;


    /**
     * 是否立即清除副本,默認(rèn)立即清除副本數(shù)據(jù)。
     * 如果獲取數(shù)據(jù)方式為 THREAD_LOCAL,后續(xù)還需要使用該參數(shù),開發(fā)自行清除
     */
    boolean isFlushThreadLocal() default true;

?2.1.1切面對應(yīng)枚舉

@AllArgsConstructor
public enum CheckInstructionEditTypeEnum {
    SALE("SALE","銷售", "2"),
    MARKET("MARKET","做市", "1"),
    BID("BID","中標(biāo)", "2");
    private String instructionCode;
    /**
     * 功能名稱
     */
    private String name;
    /**
     * 組合分類標(biāo)簽值
     */
    private String orgType;

    public String getInstructionCode() {
        return instructionCode;
    }

    public String getName() {
        return name;
    }

    public String getOrgType() {
        return orgType;
    }

    /**
     * 參數(shù)類型來源
     * DEFAULT_KEY    入?yún)?根據(jù)對象的默認(rèn)key 反射獲取,pid,pidList,pids,userId
     * ANNOTATION   入?yún)⒏鶕?jù)注解獲取 和 @BindingParam 配合使用,可以是方法入?yún)⒆⒔饣蛘邔傩宰⒔?,推薦使用
     * THREAD_LOCAL  副本(需要提前塞值)
     */
    public enum DataSourceEnum {
        DEFAULT_KEY,ANNOTATION,THREAD_LOCAL
    }

    /**
     * DataSourceEnum 為 DEFAULT_KEY 時,默認(rèn)獲取的key
     */
    public enum DataSourceDefaultKeyEnum {
        pid,pidList,pids,userId
    }

    /**
     *   CURRENT 校驗(yàn)當(dāng)前數(shù)據(jù)
     *   GROUP   校驗(yàn)整組數(shù)據(jù),eg:索引號,code。組合下達(dá)指令時,可能 會整組索引指令下達(dá),需校驗(yàn)整組數(shù)據(jù)
     */
    public enum CheckDataTypeEnum{
        CURRENT,GROUP
    }

    /**
     * 操作類型,用來錯誤信息提示
     */
    @AllArgsConstructor
    public enum OperateTypeEnum{
        ADD("ADD","新增"),
        UPDATE("UPDATE","修改"),
        DELETE("DELETE","刪除"),
        RELEASE("RELEASE","下達(dá)"),
        DECILITER("DECILITER","拆合單");
        private String code;
        private String name;

        /**
         * 操作 code
         * @return
         */
        public String getCode() {
            return code;
        }
        /**
         * 操作名稱
         * @return
         */
        public String getName() {
            return name;
        }
    }
}
?2.2 聲明綁定參數(shù)注解
/**
 * @author WANGCHENG
 * @version 1.0
 * @Description: 標(biāo)注入?yún)⒌臄?shù)據(jù)類型,配合檢驗(yàn)組合權(quán)限
 * @date 2023/08/04 20:12
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface BindingParam {
    BindingParamTypeEnum value();
}

綁定參數(shù)對應(yīng)枚舉

public enum BindingParamTypeEnum {
    pid,pidList,userId,isConfirm
}

3 切面邏輯


@Aspect
@Component
@Slf4j
public class CheckInstructionAuthAspect {
    // 切點(diǎn)
    @Pointcut(value = "@annotation(com.dsd.study.annotion.CheckInstructionAuth)")
    public void pointcut() {}

    /**
     * 切點(diǎn)配置,CheckCombinedEditAuth 注解的地方
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("pointcut()")
    public Object CheckCombinedEditAuth(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        String targetClassName = joinPoint.getTarget().getClass().getCanonicalName();
        String targetMethodName = joinPoint.getSignature().getName();
        String target=targetClassName+"#"+targetMethodName;
        log.info("校驗(yàn)指令權(quán)限目標(biāo)方法為={}",target);
        if(!editAuthSwitch()){
            return joinPoint.proceed();
        }
        //獲取方法,此處可將signature強(qiáng)轉(zhuǎn)為MethodSignature
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //參數(shù)注解,1維是參數(shù),2維是注解
        CheckInstructionAuth checkInstructionAuth = method.getAnnotation(CheckInstructionAuth.class);
        Object[] args = joinPoint.getArgs();
        log.info("校驗(yàn)指令權(quán)限目標(biāo)方法入?yún)?{},={}",target,JSON.toJSONString(args));
        CheckInstructionEditTypeEnum checkInstructionEditTypeEnum = checkInstructionAuth.instructionType();
        Map<String, Object> paramData =new HashMap<>();
        if(CheckInstructionEditTypeEnum.SALE.equals(checkInstructionEditTypeEnum)){
            paramData=analysisDataSource(args,checkInstructionAuth,method);
        }
        log.info("{}校驗(yàn)指令權(quán)限解析入?yún)?{}",target,JSON.toJSONString(paramData));
        String userId =(String) paramData.get(BindingParamTypeEnum.userId.name());
        List<String> pidList =(List<String>)paramData.get(BindingParamTypeEnum.pidList.name());
        if(StringUtils.isBlank(userId)){
            log.info("CheckCombinedEditAuthAspect#CheckCombinedEditAuth--->未查詢到用戶信息");
            throw new BusinessException("未查詢到用戶信息");
        }
        List<String> userAuthList = getUserAuthList(userId, checkInstructionAuth);
        if(pidList==null || pidList.size()==0){
            log.info("CheckCombinedEditAuthAspect#CheckCombinedEditAuth--->未查詢到校驗(yàn)的指令");
            throw new BusinessException("未查詢到校驗(yàn)的指令");
        }
        List<String> checkDataList = getCheckData(pidList, checkInstructionAuth);
        //權(quán)限對比
        if(!compareAuth(userAuthList,checkDataList)){
            log.info("userId={},權(quán)限對比,userAuthList={},checkDataList={}",
                    JSON.toJSONString(userAuthList),JSON.toJSONString(checkDataList));
            checkDataList.removeAll(userAuthList);
            if(checkDataList.size()>0){
                //數(shù)據(jù)權(quán)限大于用戶擁有權(quán)限
                List<String> combinedNameList = getCombinedNameByVcRemarksKey(checkDataList);
                //沒有權(quán)限的組合
                String combinedNameListStr = combinedNameList.stream()
                        .collect(Collectors.joining(","));
                String operateName = checkInstructionAuth.operateType().getName();
                log.info("userId={},權(quán)限不相等,沒有"+combinedNameListStr+"組合的操作權(quán)限,不允許{}",userId,operateName);
                //沒有XX組合的操作權(quán)限,不允許新增
                throw new BusinessException("沒有"+combinedNameListStr+"組合的操作權(quán)限,不允許"+operateName);
            }
        }
        log.info("{}耗時為={}毫秒",target,System.currentTimeMillis()-startTime);
        return joinPoint.proceed();
    }

    /**
     * 根據(jù)組合code獲取組合名稱
     * @param vcRemarksKeyList
     * @return
     */
    private List<String> getCombinedNameByVcRemarksKey(List<String> vcRemarksKeyList){
        //偽代碼
        return new ArrayList<>();
    }

    /**
     * 比較List<string> 是否相等
     * @param userAuthList
     * @param checkDataList
     * @return
     */
    private boolean compareAuth(List<String> userAuthList,List<String> checkDataList){
        if(userAuthList.size()!=checkDataList.size()){
            return false;
        }
        String[] userAuthArry = userAuthList.toArray(new String[]{});
        String[] checkDataArry = checkDataList.toArray(new String[]{});
        Arrays.sort(userAuthArry);
        Arrays.sort(checkDataArry);
        return Arrays.equals(userAuthArry, checkDataArry);
    }
    /**
     * 解析入?yún)?shù)據(jù),拿到需要的入?yún)?     * @param args
     * @return
     */
    private Map<String,Object> analysisDataSource(Object[] args, CheckInstructionAuth checkCombinedEditAuth, Method method)
            throws IllegalAccessException, BusinessException {
        Map<String,Object> result=new HashMap<>();
        String operateId=null;
        List<String> pidList=new ArrayList<>();
        CheckInstructionEditTypeEnum.DataSourceEnum dataSource = checkCombinedEditAuth.dataSource();
        try {
            if(CheckInstructionEditTypeEnum.DataSourceEnum.DEFAULT_KEY.equals(dataSource)){
                //反射,獲取默認(rèn)的 key
                for(Object obj:args){
                    if(obj instanceof Map){
                        Map<String,Object> objMap=(Map<String,Object>)obj;
                        getParamByDefaultKey(objMap,result);
                    }else{
                        //自定義對象默認(rèn)key
                        Field[] fields = obj.getClass().getDeclaredFields();
                        for(Field field:fields){
                            field.setAccessible(true);
                            String name = field.getName();
                            if(CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.userId.name().equals(name)){
                                if(obj instanceof String){
                                    operateId=(String)field.get(obj);
                                }
                            }else if(CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.pid.name().equals(name) ||
                                    CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.pidList.name().equals(name) ||
                                    CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.pids.name().equals(name)){
                                // key 為 pid,pids,pidList
                                if(obj instanceof String){
                                    pidList.add((String)field.get(obj));
                                }else if(obj instanceof List){
                                    pidList.addAll((List)field.get(obj));
                                }
                            }
                        }
                    }
                }
            }else if(CheckInstructionEditTypeEnum.DataSourceEnum.ANNOTATION.equals(dataSource)){
                //注解,入?yún)⒓幼⒔?                Annotation[][] parameterAnnotations = method.getParameterAnnotations();
                int index = 0;
                for(Annotation[] annotationx:parameterAnnotations){
                    Object param=args[index];
                    for(Annotation annotationy:annotationx){
                        if(annotationy instanceof BindingParam){
                            BindingParam bindingParam=(BindingParam)annotationy;
                            getParamByBindingParam(bindingParam, param,result);
                        }
                    }
                    index++;
                }
                //注解,屬性加注解,注意:會覆蓋入?yún)⒆⒔獾娜≈?                for(Integer i=0;i<args.length;i++){
                    Object obj=args[i];
                    Field[] fields = obj.getClass().getDeclaredFields();
                    for(Field field:fields){
                        field.setAccessible(true);
                        //如果字段上有自定義注解@BindingParam
                        BindingParam bindingParam = field.getAnnotation(BindingParam.class);
                        getParamByBindingParam(bindingParam, field.get(obj),result);
                    }
                }
            }else if(CheckInstructionEditTypeEnum.DataSourceEnum.THREAD_LOCAL.equals(dataSource)){
                operateId = (String) ThreadLocalUtils.get(BindingParamTypeEnum.userId.name());
                List<String> pidListResult = (List<String>)ThreadLocalUtils.get(BindingParamTypeEnum.pidList.name());
                pidList.addAll(pidListResult);
            }
        } catch (BusinessException e) {
            throw e;
        } finally {
            if(CheckInstructionEditTypeEnum.DataSourceEnum.THREAD_LOCAL.equals(dataSource) &&
                    checkCombinedEditAuth.isFlushThreadLocal()){
                ThreadLocalUtils.delete(BindingParamTypeEnum.userId.name());
                ThreadLocalUtils.delete(BindingParamTypeEnum.pidList.name());
            }
        }
        if(StringUtils.isNotBlank(operateId)){
            result.put(BindingParamTypeEnum.userId.name(),operateId);
        }
        if(pidList.size()>0){
            result.put(BindingParamTypeEnum.pidList.name(),pidList);
        }
        return result;
    }

    /**
     * 入?yún)镸ap 解析數(shù)據(jù)
     * @param map
     * @param result
     * @return
     */
    private Map<String,Object> getParamByDefaultKey(Map<String,Object> map,Map<String,Object> result){
        if(map==null){
            return result;
        }
        for(Map.Entry<String,Object> mapx:map.entrySet()){
            String key = mapx.getKey();
            Object value = mapx.getValue();
            if(value==null){
                continue;
            }
            if(CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.userId.name().equals(key)){
                if(value instanceof String){
                    result.put(CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.userId.name(),(String)value);
                }
            } else if(CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.pid.name().equals(key)){
                if(value instanceof String){
                    result.put(CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.pidList.name()
                            ,Arrays.asList((String)value));
                }
            } else if(CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.pidList.name().equals(key) ||
                    CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.pids.name().equals(key)){
                if(value instanceof List){
                    if(value instanceof List){
                        List<Object> objList=(List<Object>)value;
                        if(objList.size()>0 && objList.get(0) instanceof String){
                            result.put(CheckInstructionEditTypeEnum.DataSourceDefaultKeyEnum.pidList.name()
                                    ,(List<String>)value);
                        }
                    }
                }
            }
        }
        return result;
    }

    /**
     * 根據(jù)注解獲取入?yún)?     * @param bindingParam
     * @param param
     * @return
     */
    private Map<String,Object> getParamByBindingParam(BindingParam bindingParam, Object param, Map<String,Object> result)
            throws BusinessException {
        if(bindingParam==null){
            log.info("沒有 @BindingParam 綁定參數(shù)");
            return result;
        }
        if(bindingParam!=null){
            BindingParamTypeEnum value = bindingParam.value();
            if(BindingParamTypeEnum.pid.equals(value)){
                if(param instanceof String){
                    result.put(BindingParamTypeEnum.pidList.name()
                            , Arrays.asList((String)param).stream().collect(Collectors.toList()));
                }else{
                    log.info("@BindingParam 注解類型使用錯誤,pid只能綁定String類型");
                    throw new BusinessException("@BindingParam 注解類型使用錯誤,pid只能綁定String類型");
                }

            }else if(BindingParamTypeEnum.pidList.equals(value)){
                if(param instanceof List){
                    List<Object> objList=(List)param;
                    if(objList!=null && objList.size()>0){
                        if(objList.get(0) instanceof String){
                            result.put(BindingParamTypeEnum.pidList.name(),(List<String>)param);
                        }else {
                            log.info("@BindingParam 注解類型值使用錯誤,pidList只能綁定List<String>類型");
                            throw new BusinessException("@BindingParam 注解類型值使用錯誤,pidList只能綁定List<String>類型");
                        }
                    }
                }else{
                    log.info("@BindingParam 注解類型值使用錯誤,pidList只能綁定List類型");
                    throw new BusinessException("@BindingParam 注解類型值使用錯誤,pidList只能綁定List類型");
                }
            }else if(BindingParamTypeEnum.userId.equals(value)){
                if(param instanceof String){
                    result.put(BindingParamTypeEnum.userId.name(),(String)param);
                }else{
                    log.info("@BindingParam 注解類型值使用錯誤,userId只能綁定String類型");
                    throw new BusinessException("@BindingParam 注解類型值使用錯誤,userId只能綁定String類型");
                }
            }
        }
        return result;
    }

    /**
     * 用戶存在的編輯組合權(quán)限
     * @param userId
     * @param checkCombinedEditAuth
     * @return
     */
    private List<String> getUserAuthList(String userId,CheckInstructionAuth checkCombinedEditAuth){
        //獲取用戶權(quán)限,業(yè)務(wù)代碼
        return new ArrayList<>();
    }

    /**
     * 需要校驗(yàn)的組合數(shù)據(jù)
     * @param pidList
     * @param checkCombinedEditAuth
     * @return
     */
    private List<String> getCheckData(List<String> pidList, CheckInstructionAuth checkCombinedEditAuth){
        CheckInstructionEditTypeEnum checkInstructionEditTypeEnum = checkCombinedEditAuth.instructionType();
        List<String> checkDataList=new ArrayList<>();
        if(CheckInstructionEditTypeEnum.SALE.equals(checkInstructionEditTypeEnum)){
            //獲取需要校驗(yàn)的數(shù)據(jù)
        }
        //校驗(yàn)其它
        return checkDataList;
    }

    /**
     * 校驗(yàn)權(quán)限開關(guān),redis 控制。默認(rèn)開啟=1;
     * @return
     */
    private boolean editAuthSwitch(){
        return true;
    }
}

?4涉及的Util?

4.1 ThreadLocalUtil?
public class ThreadLocalUtils {
    private static final ThreadLocal<Map<String, Object>> THREAD_LOCAL =
            ThreadLocal.withInitial(() -> new ConcurrentHashMap<>(16));

    /**
     * 獲取到ThreadLocal中值
     *
     * @return ThreadLocal存儲的是Map
     */
    public static Map<String, Object> getThreadLocal() {
        return THREAD_LOCAL.get();
    }

    /**
     * 從ThreadLocal中的Map獲取值
     *
     * @param key Map中的key
     * @param <T> Map中的value的類型
     * @return Map中的value值 可能為空
     */
    public static <T> T get(String key) {
        return get(key, null);
    }

    /**
     * 從ThreadLocal中的Map獲取值
     *
     * @param key          Map中的key
     * @param defaultValue Map中的value的為null 是 的默認(rèn)值
     * @param <T>          Map中的value的類型
     * @return Map中的value值 可能為空
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(String key, T defaultValue) {
        Map<String, Object> map = THREAD_LOCAL.get();
        if (MapUtils.isEmpty(map)) {
            return null;
        }
        return (T) Optional.ofNullable(map.get(key)).orElse(defaultValue);
    }

    /**
     * ThreadLocal中的Map設(shè)置值
     *
     * @param key   Map中的key
     * @param value Map中的value
     */
    public static void set(String key, Object value) {
        Map<String, Object> map = THREAD_LOCAL.get();
        map.put(key, value);
    }

    /**
     * ThreadLocal中的Map 添加Map
     *
     * @param keyValueMap 參數(shù)map
     */
    public static void set(Map<String, Object> keyValueMap) {
        Map<String, Object> map = THREAD_LOCAL.get();
        map.putAll(keyValueMap);
    }

    /**
     * 刪除ThreadLocal中的Map 中的value
     *
     * @param key Map中的key
     */
    public static void delete(String key) {
        Map<String, Object> map = THREAD_LOCAL.get();
        if (MapUtils.isEmpty(map)) {
            return;
        }
        map.remove(key);
    }

    /**
     * 刪除ThreadLocal中的Map
     */
    public static void remove() {
        THREAD_LOCAL.remove();
    }

    /**
     * 從ThreadLocal中的Map獲取值 根據(jù)可key的前綴
     *
     * @param prefix key 的前綴
     * @param <T>    Map中的value的類型
     * @return 符合條件的Map
     */
    @SuppressWarnings("unchecked")
    public static <T> Map<String, T> fetchVarsByPrefix(String prefix) {
        Map<String, T> vars = new HashMap<>(16);
        if (StringUtils.isBlank(prefix)) {
            return vars;
        }
        Map<String, Object> map = THREAD_LOCAL.get();
        if (MapUtils.isEmpty(map)) {
            return vars;
        }
        return map.entrySet().stream().filter(test -> test.getKey().startsWith(prefix))
                .collect(Collectors.toMap(Map.Entry::getKey, time -> (T) time.getValue()));
    }

    /**
     * 刪除ThreadLocal中的Map 中的Value  按 Map中的Key的前綴
     *
     * @param prefix Map中的Key的前綴
     */
    public static void deleteVarsByPrefix(String prefix) {
        if (StringUtils.isBlank(prefix)) {
            return;
        }
        Map<String, Object> map = THREAD_LOCAL.get();
        if (MapUtils.isEmpty(map)) {
            return;
        }
        map.keySet().stream().filter(o -> o.startsWith(prefix)).collect(Collectors.toSet()).forEach(map::remove);
    }
}
4.2? 自定義異常
public class BusinessException extends Exception{
    private static final long serialVersionUID = -3463054564635276929L;
    /**
     * 錯誤碼
     */
    private String errCode;

    /**
     * 錯誤描述
     */
    private String errDesc;

    public BusinessException() {
        super();
    }

    public BusinessException(String errDesc) {
        super(errDesc);
        this.errDesc = errDesc;
    }

    public BusinessException(String errCode, String errDesc) {
        super(errCode);
        this.errCode = errCode;
        this.errDesc = errDesc;
    }

    public String getErrCode() {
        return errCode;
    }

    public String getErrDesc() {
        return errDesc;
    }
}

5 使用示例

@Service
public class TestServiceImpl implements TestService {
    @CheckInstructionAuth(operateType = CheckInstructionEditTypeEnum.OperateTypeEnum.RELEASE)
    @Override
    public void getData(Map<String, Object> map,@BindingParam(BindingParamTypeEnum.pidList) String userId) {
        System.out.println("testAnnotation");
    }


    @CheckInstructionAuth(operateType = CheckInstructionEditTypeEnum.OperateTypeEnum.RELEASE)
    @Override
    public void getData(List<UserDto> userDto) {
        System.out.println("testAnnotation");
    }
}

文章來源地址http://www.zghlxwxcb.cn/news/detail-634990.html

到了這里,關(guān)于spring自定義注解+aop+@BindingParam的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包