记一次大物流查询插件的开发

来项目组已经一个多月了,oms、wms、tms都有涉及,大物流系统采用原始的hibernate操作数据库,并进行了一系列的封装。单表增删改查比较方便,直接操作对象,和jpa、mybaits plus类似。涉及到多表查询,用供应商的话来讲得写三件套,如下:
在这里插入图片描述
作为比较懒的我,这是一种煎熬,也就是上周五我萌生了,依托现有框架,进行无侵入性扩展。类似jpa、mybaits plus,实行注解式sql查询。
我的思路是

1.弄一个aop切面,切入包名下的包含@Query的接口方法。
2.通过反射获取方法的返回值,和方法的参数对象。
3.实现变量式传参,和判断脚本的识别,用于拼接sql。
4.实现springboot代理,自动实现接口类的实现方法,从而减少写实现类的步骤。
5.那就是开干。

在这里吟诗一首:路漫漫其修远兮,吾将上下而求索!!!!

实现用于aop切面的切点的方法主键Query*
package com.sinoservices.tro.annotation;

/**
 *
 * @author zhanqi
 * @since 2021/7/2 20:37
 */

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Query {
    String value();
}

参数Param("")用于sql参数引用
package com.sinoservices.tro.annotation;

import java.lang.annotation.*;

/**
 * @author zhanqi
 * @since 2021/7/2 20:39
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Param {
    String value();
}

实现aop类,里面包含了map大写转驼峰,sql变量,sql处理js逻辑脚本
主要的技术就是反射
package com.sinoservices.tro.aspect;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sinoservices.tro.annotation.Param;
import com.sinoservices.tro.annotation.Query;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author zhanqi
 * @since 2021/7/2 11:07
 * 啥也不说了,就是秀
 */
@Aspect
@Component
@Slf4j
public class QueryAspect {
    private static final char UNDERLINE = '_';
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Pointcut("execution(* com.sinoservices..*(..))&&@annotation(com.sinoservices.tro.annotation.Query)")
    public void fun() {
    }

    @Around("fun()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        Query query = method.getAnnotation(Query.class);
        String sqlTemplate = query.value();
        List<String> sqlParamList = getSqlParamList(sqlTemplate);//获取sql需要替换的参数
        // 参数值
        Object[] args = pjp.getArgs();
        AtomicBoolean isParamBeanModelFlag = new AtomicBoolean(false);//判断是否参数是对象
        if (checkIsBeanModel(args[0]) && args.length == 1) {
            isParamBeanModelFlag.set(true);
        }

        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
        sqlParamList.forEach(sqlParam -> {
            if (isParamBeanModelFlag.get()) {
                JSONObject json = (JSONObject) JSON.toJSON(args[0]);
                putMapByParamBeanModel(json, sqlParam, map);
            } else {
                createSqlReplaceMap(method, sqlParam, map, args);
            }

        });
        for (String key : map.keySet()) {
            sqlTemplate = sqlTemplate.replace(key, map.get(key).toString());
        }
        sqlTemplate = redJsTemp(sqlTemplate);//读取js模版
        System.out.println("sqlTemplate:    " + sqlTemplate);
        Type returnType = method.getGenericReturnType();

        //判断是否是list
        if (returnType.getTypeName().contains("java.util.List")) {
            List list = new ArrayList();
            List<?> jdbcResult = jdbcTemplate.queryForList(sqlTemplate);
            if (jdbcResult != null && !jdbcResult.isEmpty()) {
                for (Object e : jdbcResult) {
                    Object bean = createListBean(returnType);
                    BeanUtils.populate(bean, formatHumpName(keyToLowerCase((Map<String, Object>) e)));
                    list.add(bean);
                }
            }
            return list;
        } else {
            Object obj = createBean(returnType);
            List<?> jdbcResult = jdbcTemplate.queryForList(sqlTemplate);
            if (jdbcResult != null && !jdbcResult.isEmpty()) {
                Map<String, Object> bankMap = formatHumpName(keyToLowerCase((Map<String, Object>) jdbcResult.get(0)));
                BeanUtils.populate(obj, bankMap);
                return obj;
            }
        }
        return null;
    }

    private Map<String, Object> formatHumpName(Map<String, Object> map) {
        Map<String, Object> newMap = new HashMap<>();
        Iterator<Map.Entry<String, Object>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Object> entry = it.next();
            String key = entry.getKey();
            String newKey = underlineToCamel(key);
            newMap.put(newKey, entry.getValue());
        }
        return newMap;
    }

    /**
     * 下划线转驼峰
     *
     * @param param
     * @return
     */
    private String underlineToCamel(String param) {
        if (param == null || "".equals(param.trim())) {
            return "";
        }
        int len = param.length();
        StringBuilder sb = new StringBuilder(len);
        for (int i = 0; i < len; i++) {
            char c = param.charAt(i);
            if (c == UNDERLINE) {
                if (++i < len) {
                    sb.append(Character.toUpperCase(param.charAt(i)));
                }
            } else {
                sb.append(Character.toLowerCase(param.charAt(i)));
            }
        }
        return sb.toString();
    }


    /**
     * map Key转小写
     *
     * @param orgMap
     * @return
     */
    private Map<String, Object> keyToLowerCase(Map<String, Object> orgMap) {
        Map<String, Object> resultMap = new HashMap<>();

        if (orgMap == null || orgMap.isEmpty()) {
            return resultMap;
        }

        Set<Map.Entry<String, Object>> entrySet = orgMap.entrySet();
        for (Map.Entry<String, Object> entry : entrySet) {
            String key = entry.getKey();
            Object value = entry.getValue();
            resultMap.put(key.toLowerCase(), value);
        }

        return resultMap;
    }
    

    /**
     * @param sqlTemplate 注解里面的sql模版
     * @return 返回需要替换的参数list
     */
    private List<String> getSqlParamList(String sqlTemplate) {
        List<String> sqlParamList = new ArrayList<>();
        String reg = "\\#\\{(.*?)\\}";//定义正则表达式
        Pattern patten = Pattern.compile(reg);//编译正则表达式
        Matcher matcher = patten.matcher(sqlTemplate);// 指定要匹配的字符串

        while (matcher.find()) { //此处find()每次被调用后,会偏移到下一个匹配
            sqlParamList.add(matcher.group().trim());//获取当前匹配的值
        }
        return sqlParamList;
    }

    /**
     * 创建方法参数是对象的逻辑处理
     *
     * @param json     对象转的json
     * @param sqlParam sql参数
     * @param map      需要替换的sql模版对应的值
     */
    private void putMapByParamBeanModel(JSONObject json, String sqlParam, LinkedHashMap<String, Object> map) {
        String matcherStr = getMatcher(sqlParam, 1);
        String[] filedByIns = matcherStr.split(":");
        if (filedByIns.length > 1) {
            json.forEach((k, v) -> {
                createSqlReplaceMapByIn(sqlParam, map, filedByIns, k, v);
            });
        } else {
            json.forEach((k, v) -> {
                if (k.equals(matcherStr)) {
                    map.put(sqlParam.trim(), replaceSqlByObjType(v));
                }
            });
        }
    }

    /**
     * 创建需要替换的sql模版对应的值(数类型是普通类型))
     *
     * @param isNotBeanValue 如果不是bean,传入的值(isBean=true,此处为null)
     * @param sqlParam       sql参数
     * @param map            需要替换的sql模版对应的值
     */
    private void createSqlReplaceMap(Object isNotBeanValue, String sqlParam, String matcherStr, String paramas, LinkedHashMap<String, Object> map) {
        if (matcherStr.equals(paramas)) {
            map.put(sqlParam.trim(), replaceSqlByObjType(isNotBeanValue));
        }
    }

    /**
     * 创建需要替换的sql模版对应的值(数类型是集合类型的)in(121,212)
     *
     * @param sqlParam   sql参数
     * @param map        需要替换的sql模版对应的值
     * @param filedByIns 参数类型是集合
     * @param key
     * @param value
     */
    private void createSqlReplaceMapByIn(String sqlParam, LinkedHashMap<String, Object> map, String[] filedByIns, Object key, Object value) {
        if (key.equals(filedByIns[1])) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("(");
            JSONArray arr = JSON.parseArray(JSON.toJSONString(value));
            AtomicInteger index = new AtomicInteger();
            arr.forEach(o -> {
                index.set(index.getAndIncrement() + 1);
                stringBuilder.append(replaceSqlByObjType(o));
                if (arr.size() - index.get() > 0) {
                    stringBuilder.append(",");
                }
            });
            stringBuilder.append(")");
            map.put(sqlParam.trim(), stringBuilder.toString());

        }
    }

    /**
     * @param method   方法this
     * @param sqlParam sql参数
     * @param map      需要替换的sql模版对应的值
     * @param args     method里面所有参数对象的值
     */
    private void createSqlReplaceMap(Method method, String sqlParam, LinkedHashMap<String, Object> map, Object[] args) {
        String matcherStr = getMatcher(sqlParam, 1);

        Annotation[][] pas = method.getParameterAnnotations();
        String[] params = new String[pas.length];
        for (int i = 0; i < pas.length; i++) {
            Param pa = (Param) pas[i][0];
            params[i] = pa.value();
        }

        for (int i = 0; i < args.length; i++) {
            String[] filedByIns = matcherStr.split(":");
            if (filedByIns.length > 1) {
                createSqlReplaceMapByIn(sqlParam, map, filedByIns, params[i], args[i]);
            } else {
                if (args[i].getClass().isArray() || args[i] instanceof Collection<?> || args[i] instanceof Map<?, ?>) {
                    continue;
                }
                createSqlReplaceMap(args[i], sqlParam, matcherStr, params[i], map);
            }
        }
    }

    /**
     * 查询list<?>里面的bean完整包名
     *
     * @param ListBeanName 反射获取的list类型名称
     * @return
     */
    private String findListInBeanName(String ListBeanName) {
        String regex_class_list_bean_name = "\\<(.*?)\\>";
        Pattern p = Pattern.compile(regex_class_list_bean_name);
        Matcher m = p.matcher(ListBeanName);
        while (m.find()) {
            return m.group(1);
        }
        return null;
    }

    /**
     * 根据obj类型判断拼接sql需不需要加''
     *
     * @param obj
     * @return
     */
    private Object replaceSqlByObjType(Object obj) {
        if (obj instanceof Integer) {
            return obj;
        } else if (obj instanceof Double) {
            return obj;
        } else if (obj instanceof Float) {
            return obj;
        } else if (obj instanceof Long) {
            return obj;
        } else if (obj instanceof Boolean) {
            return obj;
        } else if (obj instanceof Date) {
            return obj;
        } else {
            return "'" + obj + "'";
        }
    }

    /**
     * 判断是不是bean
     *
     * @param obj
     * @return
     */
    private boolean checkIsBeanModel(Object obj) {
        if (obj instanceof Integer) {
            return false;
        } else if (obj instanceof String) {
            return false;
        } else if (obj instanceof Double) {
            return false;
        } else if (obj instanceof Float) {
            return false;
        } else if (obj instanceof Long) {
            return false;
        } else if (obj instanceof Boolean) {
            return false;
        } else if (obj instanceof Date) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * 根据类型创建同类型的list
     *
     * @param returnType
     * @return
     * @throws Throwable
     */
    private List createList(Type returnType) throws Throwable {
        //截取list里面的bean对象名称
        String beanName = findListInBeanName(returnType.getTypeName());
        Class classType = Class.forName(beanName);
        //创建bean对象
        Object obj = classType.newInstance();
        //建立对象列表list(List同样反射,越过泛型检查)
        List list = new ArrayList();
        Method m = list.getClass().getMethod("add", Object.class);
        m.invoke(list, obj);
        return list;
    }

    /**
     * 根据类型创建同类型的list里面的bean
     *
     * @param returnType
     * @return
     * @throws Throwable
     */
    private Object createListBean(Type returnType) throws Throwable {
        //截取list里面的bean对象名称
        String beanName = findListInBeanName(returnType.getTypeName());
        Class classType = Class.forName(beanName);
        //创建bean对象
        Object obj = classType.newInstance();

        return obj;
    }

    /**
     * 根据类型创建bean
     *
     * @param beanType
     * @return
     * @throws Throwable
     */
    private Object createBean(Type beanType) throws Throwable {
        Class classType = Class.forName(beanType.getTypeName());
        Object obj = classType.newInstance();
        return obj;
    }

    /**
     * 查找#{}中间的字符
     *
     * @param str
     * @param index 0全取 1查找#{}中间的字符
     * @return
     */
    private String getMatcher(String str, Integer index) {
        String reg = "\\#\\{(.*?)\\}";
        Pattern patten = Pattern.compile(reg);
        Matcher matcher = patten.matcher(str);

        while (matcher.find()) {
            return matcher.group(index).trim();
        }
        return null;
    }

    /**
     * 读取js模版
     *
     * @param str
     * @return
     */
    private String redJsTemp(String str) {
        String regJs = "\\<js>(.*?)\\</js>";
        Pattern patten = Pattern.compile(regJs);
        Matcher matcher = patten.matcher(str);
        while (matcher.find()) {
            String jsOldStr = matcher.group();
            if (matcher.group(1) != null) {
                String jsNewStr = " function add(){  " + matcher.group(1) + " } ";
                str = str.replace(jsOldStr, exeJs(jsNewStr));

            }

        }
        return str;
    }

    /**
     * 执行js脚本
     *
     * @param jsStr
     * @return
     */
    private String exeJs(String jsStr) {
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName("JavaScript");
        //执行js脚本定义函数
        try {
            engine.eval(jsStr);
            Invocable invocable = (Invocable) engine;
            Object res = null;
            try {
                res = invocable.invokeFunction("add");
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            System.out.println(res);
            return (String) res;
        } catch (ScriptException e) {
            e.printStackTrace();
        }

        return "";
    }

}




下面就是springboot动态代理,自动注册了

首先我们建一个BaseQueryService,然后后续继承这个的接口类实现动态代理注册

package com.sinoservices.tro.service;

/**
 * @author zhanqi
 * @since 2021/7/3 21:40
 */
public interface BaseQueryService {
    void ok();
}

新建BaseQueryService的实现类
package com.sinoservices.tro.service;

/**
 * @author zhanqi
 * @since 2021/7/3 21:41
 */
public class DefaultQueryServiceImpl implements BaseQueryService {
    @Override
    public void ok() {
        System.out.println("ok");
    }
}

定义代理类实现工厂ServiceProxyFactoryBean<T>
package com.sinoservices.tro.proxy;

import org.springframework.beans.factory.FactoryBean;

import java.lang.reflect.Proxy;

/**
 * 定义代理类实现工厂
 * @author zhanqi
 * @since 2021/7/3 21:55
 */
public class ServiceProxyFactoryBean<T> implements FactoryBean<T> {
    private Class<T> interfaces;

    public ServiceProxyFactoryBean(Class<T> interfaces) {
        this.interfaces = interfaces;
    }

    @Override
    @SuppressWarnings("unchecked")
    public T getObject() throws Exception {
        return (T) Proxy.newProxyInstance(interfaces.getClassLoader(), new Class[]{interfaces},
                new ServiceProxy<>(interfaces));
    }

    @Override
    public Class<?> getObjectType() {
        return interfaces;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

定义接口扫描类ServiceInterfacesScanner
package com.sinoservices.tro.proxy;

import com.sinoservices.tro.service.BaseQueryService;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.type.AnnotationMetadata;

import java.util.Arrays;
import java.util.Set;

/**
 * 定义接口扫描类
 * @author zhanqi
 * @since 2021/7/3 22:03
 */
public class ServiceInterfacesScanner extends ClassPathBeanDefinitionScanner {

    ServiceInterfacesScanner(BeanDefinitionRegistry registry) {
        //false表示不使用ClassPathBeanDefinitionScanner默认的TypeFilter
        super(registry, false);
    }

    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        this.addFilter();
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        if (beanDefinitionHolders.isEmpty()) {
            throw new NullPointerException("No interfaces");
        }
        this.createBeanDefinition(beanDefinitionHolders);
        return beanDefinitionHolders;
    }

    /**
     * 只扫描顶级接口
     * @param beanDefinition bean定义
     * @return boolean
     */
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        AnnotationMetadata metadata = beanDefinition.getMetadata();
        String[] interfaceNames = metadata.getInterfaceNames();
        return metadata.isInterface() && metadata.isIndependent()&& Arrays.asList(interfaceNames).contains(BaseQueryService.class.getName());
    }

    /**
     * 扫描所有类
     */
    private void addFilter() {
        addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
    }

    /**
     * 为扫描到的接口创建代理对象
     *
     * @param beanDefinitionHolders beanDefinitionHolders
     */
    private void createBeanDefinition(Set<BeanDefinitionHolder> beanDefinitionHolders) {
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
            GenericBeanDefinition beanDefinition = ((GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition());
            //将bean的真实类型改变为FactoryBean
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            beanDefinition.setBeanClass(ServiceProxyFactoryBean.class);
            beanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
        }
    }

}

定义注册类
package com.sinoservices.tro.proxy;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;

/**
 * 定义注册类
 * @author zhanqi
 * @since 2021/7/3 21:44
 */
public class ProxyRegister implements BeanDefinitionRegistryPostProcessor {
    private String basePackage;

    public ProxyRegister(String basePackage) {
        this.basePackage = basePackage;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        if (basePackage.equals("")||basePackage==null) {
            return;
        }
        ServiceInterfacesScanner scanner = new ServiceInterfacesScanner(registry);
        scanner.doScan(basePackage);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

    }
}

定义一个代理类
package com.sinoservices.tro.proxy;

import com.sinoservices.tro.service.DefaultQueryServiceImpl;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 定义一个代理类
 * @author zhanqi
 * @since 2021/7/3 21:48
 */
public class ServiceProxy<T> implements InvocationHandler {
    private Class<T> interfaces;

    ServiceProxy(Class<T> interfaces) {
        this.interfaces = interfaces;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getDeclaringClass().equals(interfaces)) {
            System.out.println("执行您的方法:" + method.getName());
            return method.getName();
        } else {
            return method.invoke(new DefaultQueryServiceImpl(), args);
        }
    }
}
配置接口扫描路径
package com.sinoservices.tro.config;

import com.sinoservices.tro.proxy.ProxyRegister;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置接口扫描路径
 * @author zhanqi
 * @since 2021/7/3 22:16
 */
@Configuration
public class QueryConfiguration {
    @Bean
    public ProxyRegister proxyRegister() {
        return new ProxyRegister("com.sinoservices");
    }
}

现在到了展现成功的时候了

FeignClient暴露接口
    @PostMapping("/etOrderService/getList")
    List<EtOrderBean> getList(@RequestBody TestBean testBean);

    @GetMapping("/etOrderService/getModel")
    EtOrderBean getModel(@RequestParam("a") String a,@RequestParam("b") String b,@RequestParam("c") String[] c);
写接口的实现方法(testMapper就是支持注解的)
    @Autowired
    private TestQueryService testMapper;
    
   @Override
    public List<EtOrderBean>  getList(TestBean testBean) {
        return testMapper.getList(testBean);
    }

    @Override
    public EtOrderBean  getModel(String a, String b,String[] c) {
        return testMapper.getModel(a,b,c);
    }

关键的来了哈
变量用#{a}
变量是数组#{:a}根据类型自动转in()
需要判断拼接if(‘aa’!=null)return “AND ETTA_CUSTOM_EBCU_NAME_CN IN#{:c}”
就是一段js代码 注意 return 后面的用 " xxxxx "

package com.sinoservices.common.service;

import com.sinoservices.tro.annotation.Param;
import com.sinoservices.tro.annotation.Query;
import com.sinoservices.tro.service.BaseQueryService;
import com.sinoservices.tro.order.bean.EtOrderBean;
import com.sinoservices.tro.order.bean.TestBean;
import java.util.List;

/**
 * @author zhanqi
 * @since 2021/7/3 20:54
 */
public interface TestQueryService extends BaseQueryService {

    @Query("SELECT * from et_task where ETTA_ORDER_NO=#{a}  AND ETTA_LEVEL=#{b} <js>if('aa'!=null)return \"AND ETTA_CUSTOM_EBCU_NAME_CN IN#{:c}\" </js> limit 3  ")
    List<EtOrderBean> getList(TestBean testBean);

    @Query("SELECT * from et_task where ETTA_DO_NO=#{a}  AND ETTA_LEVEL=#{b}  AND ETTA_CUSTOM_EBCU_NAME_CN IN#{:c}  limit 1 ")
    EtOrderBean getModel(@Param("a") String a,@Param("b") String b, @Param("c")String[] c);
}


postman请求看效果
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值