解决黑白客户名单发布和灰度发布问题

1.开关代码

public class GraySwitchUtil {

    /**
     * 初级灰度开关判断,一刀切,不支持白名单、百分比
     * @param appKey 灰度lion配置appKey
     * @param switchConfigName 灰度lion配置,配置为boolean值
     * @param businessType 业务类型,日志、打点用
     * @return 灰度结果 true命中灰度/false未命中灰度
     */
    public static boolean isSwitchedBasic(final String appKey, final String switchConfigName, String businessType) {
        boolean switchResult = false;
        try {
            // 灰度结果
            switchResult = Lion.getBoolean(appKey, switchConfigName, false);
            return switchResult;
        } catch (Exception e) {
            log.error("[isSwitchedBasicException]灰度过程异常,默认降级为不灰度", e);
            NonIgnorableErrorMonitorUtil.logEvent("isSwitchedBasicException", "switchConfigName=" + switchConfigName + ",businessType=" + businessType);
            switchResult = false;
            return switchResult;
        } finally {
            logGrayEvent(switchConfigName, switchResult, businessType);
        }
    }

    /**
     * 高级灰度开关判断,支持黑名单、白名单、百分比
     * 首先判断黑名单灰度,如果命中黑名单则返回结果为false,黑名单配置为switchConfigNamePre+".black",类型为List<Long>
     * 其次判断白名单灰度,如果命中白名单则返回结果为true,白名单配置为switchConfigNamePre+".white",类型为List<Long>
     * 最后判断百分比灰度,如果命中百分比(尾号后两位小于配置的百分比值)则返回结果为true,百分比配置为switchConfigNamePre+".percent",类型为int(0-100之间配置,0表示全部不灰度,100表示全部灰度)
     * @param target 灰度目标,通常是门店ID、用户ID等
     * @param appKey 灰度lion配置appKey
     * @param switchConfigNamePre 灰度lion配置前缀
     * @param businessType 业务类型,日志、打点用
     * @return 灰度结果 true命中灰度/false未命中灰度
     */
    public static boolean isSwitchedAdvanced(final Long target, final String appKey, final String switchConfigNamePre, final String businessType) {
        boolean switchResult = false;
        try {
            // 黑名单
            List<Long> blackList = Lion.getList(appKey, switchConfigNamePre + ".black", Long.class, new ArrayList<>());
            // 白名单
            List<Long> whiteList = Lion.getList(appKey, switchConfigNamePre + ".white", Long.class, new ArrayList<>());
            // 百分比
            int percent = Lion.getInt(appKey, switchConfigNamePre + ".percent", 0);
            if (target == null) {
                log.error("[grayTargetIsNull]灰度目标为空,全部不灰度,target={},appKey={},switchConfigNamePre={}", target, appKey, switchConfigNamePre);
                NonIgnorableErrorMonitorUtil.logEvent("grayTargetIsNull", "switchConfigNamePre=" + switchConfigNamePre + ",bussinessType=" + businessType);
                // 如果已经切到全部灰度,则即使灰度目标为null也仍然认为是命中灰度
                switchResult = percent >= 100;
                return switchResult;
            }
            if (CollectionUtils.isNotEmpty(blackList)) {
                boolean hitBlack = blackList.contains(target);
                if (hitBlack) {
                    BusinessMonitorUtil.logEvent("grayTargetHitBlack", "switchConfigNamePre=" + switchConfigNamePre + ",businessType=" + businessType);
                    log.info("[grayTargetHitBlack]命中黑名单不灰度,target={},appKey={},switchConfigNamePre={}", target, appKey, switchConfigNamePre);
                    switchResult = false;
                    return switchResult;
                }
            }
            if (CollectionUtils.isNotEmpty(whiteList)) {
                boolean hitWhite = whiteList.contains(target);
                if (hitWhite) {
                    log.info("[grayTargetHitWhite]命中白名单灰度,target={},appKey={},switchConfigNamePre={}", target, appKey, switchConfigNamePre);
                    switchResult = true;
                    return switchResult;
                }
            }
            long tail = target % 100;
            boolean hitPercent = tail < percent;
            if (hitPercent) {
                log.info("[grayTargetHitPercent]命中百分比灰度,target={},appKey={},switchConfigNamePre={}", target, appKey, switchConfigNamePre);
                switchResult = true;
                return switchResult;
            }
            log.info("[grayTargetHitNone]没有命中灰度,target={},appKey={},switchConfigNamePre={}", target, appKey, switchConfigNamePre);
            switchResult = false;
            return switchResult;
        } catch (Exception e) {
            log.error("[isSwitchedAdvancedException]灰度过程异常,默认降级为不灰度", e);
            NonIgnorableErrorMonitorUtil.logEvent("isSwitchedAdvancedException", "switchConfigNamePre=" + switchConfigNamePre + ",businessType=" + businessType);
            switchResult = false;
            return switchResult;
        } finally {
            logGrayEvent(target, switchConfigNamePre, switchResult, businessType);
        }
    }

    /**
     * 灰度开关判断,支持黑名单、白名单、百分比
     * 开关配置为switchConfigName,类型为JSON
     * 示例:{"blackList":[],"whiteList":[],"percent":0}
     * 首先判断黑名单灰度,如果命中黑名单则返回结果为false
     * 其次判断白名单灰度,如果命中白名单则返回结果为true
     * 最后判断百分比灰度,如果命中百分比(尾号后两位小于配置的百分比值)则返回结果为true,(0-100之间配置,0表示全部不灰度,100表示全部灰度)
     * @param target 灰度目标,通常是门店ID、用户ID等
     * @param appKey 灰度lion配置appKey
     * @param switchConfigName 灰度lion配置前缀
     * @param businessType 业务类型,日志、打点用
     * @return 灰度结果 true命中灰度/false未命中灰度
     */
    public static boolean isSwitchedAdvancedByConfig(final Long target, final String appKey, final String switchConfigName, final String businessType) {
        boolean switchResult = false;
        try {
            // 1.获取灰度开关配置
            GraySwitchConfig graySwitchConfig = Lion.getBean(appKey, switchConfigName, GraySwitchConfig.class, new GraySwitchConfig());
            AssertUtil.notNull(graySwitchConfig, switchConfigName + "灰度开关NULL");
            // 2.解析百分比、黑白名单
            int percent = Optional.ofNullable(graySwitchConfig.getPercent()).orElse(0);
            List<Long> blackList = Optional.ofNullable(graySwitchConfig.getBlackList()).orElse(new ArrayList<>());
            List<Long> whiteList = Optional.ofNullable(graySwitchConfig.getWhiteList()).orElse(new ArrayList<>());
            // 3.灰度目标为空
            if (target == null) {
                log.error("[grayTargetIsNull]灰度目标为空,全部不灰度,target={},appKey={},switchConfigName={}", target, appKey, switchConfigName);
                NonIgnorableErrorMonitorUtil.logEvent("grayTargetIsNull", "switchConfigName=" + switchConfigName + ",bussinessType=" + businessType);
                // 如果已经切到全部灰度,则即使灰度目标为null也仍然认为是命中灰度
                switchResult = percent >= 100;
                return switchResult;
            }
            // 4.黑白名单处理
            if (CollectionUtils.isNotEmpty(blackList)) {
                boolean hitBlack = blackList.contains(target);
                if (hitBlack) {
                    BusinessMonitorUtil.logEvent("grayTargetHitBlack", "switchConfigName=" + switchConfigName + ",businessType=" + businessType);
                    log.info("[grayTargetHitBlack]命中黑名单不灰度,target={},appKey={},switchConfigName={}", target, appKey, switchConfigName);
                    switchResult = false;
                    return switchResult;
                }
            }
            if (CollectionUtils.isNotEmpty(whiteList)) {
                boolean hitWhite = whiteList.contains(target);
                if (hitWhite) {
                    log.info("[grayTargetHitWhite]命中白名单灰度,target={},appKey={},switchConfigName={}", target, appKey, switchConfigName);
                    switchResult = true;
                    return switchResult;
                }
            }
            // 5.百分比逻辑处理
            long tail = target % 100;
            boolean hitPercent = tail < percent;
            if (hitPercent) {
                log.info("[grayTargetHitPercent]命中百分比灰度,target={},appKey={},switchConfigName={}", target, appKey, switchConfigName);
                switchResult = true;
                return switchResult;
            }
            log.info("[grayTargetHitNone]没有命中灰度,target={},appKey={},switchConfigName={}", target, appKey, switchConfigName);
            switchResult = false;
            return switchResult;
        } catch (Exception e) {
            log.error("[isSwitchedException]灰度过程异常,默认降级为不灰度", e);
            NonIgnorableErrorMonitorUtil.logEvent("isSwitchedException", "switchConfigName=" + switchConfigName + ",businessType=" + businessType);
            switchResult = false;
            return switchResult;
        } finally {
            logGrayEvent(target, switchConfigName, switchResult, businessType);
        }

    }

    private static void logGrayEvent(String switchConfigName, boolean switched, String businessType) {
        String nameValuePairs = "TraceId=" + Tracer.getServerTracer().getTraceId() + "&localIp=" + Tracer.getServerTracer().getLocalIp();
        Cat.logEvent(BUSINESS_EVENT_TYPE_GRAY_MONITOR, switchConfigName + "_" + businessType, switched? Event.SUCCESS: "FAIL", nameValuePairs);
    }

    private static void logGrayEvent(Long target, String switchConfigName, boolean switched, String businessType) {
        String nameValuePairs = "target=" + target + "&TraceId=" + Tracer.getServerTracer().getTraceId() + "&localIp=" + Tracer.getServerTracer().getLocalIp();
        Cat.logEvent(BUSINESS_EVENT_TYPE_GRAY_MONITOR, switchConfigName + "_" + businessType, switched? Event.SUCCESS: "FAIL", nameValuePairs);
    }

    @Data
    public static class GraySwitchConfig {
        private List<Long> blackList = Lists.newArrayList();
        private List<Long> whiteList = Lists.newArrayList();
        private Integer percent = 0;
    }

}

2.Lion是作者工作公司的特有中间件,可以理解为nacos的配置文件专门抽出来做了一个功能,用来获取各种类型的配置文件(可修改为nacos配置文件获取工具类)

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.dianping.lion.client;

import com.dianping.lion.Environment;
import com.dianping.lion.client.fileconfig.FileConfigClient;
import com.dianping.lion.client.fileconfig.FileConfigManagerFactory;
import com.dianping.lion.client.http.PollManager;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import org.springframework.util.Assert;

public class Lion {
    private static ConfigManager configManager = ConfigManagerFactory.getConfigManager();
    private static ConfigRepositoryManager repositoryManager = ConfigRepositoryManager.getInstance();

    public Lion() {
    }

    /** @deprecated */
    @Deprecated
    public String getEnv() {
        return Environment.getEnv();
    }

    public String getEnvironment() {
        return Environment.getEnvironment();
    }

    public String getAppName() {
        return Environment.getAppName();
    }

    /** @deprecated */
    @Deprecated
    public static String get(String key) {
        return configManager.get(key);
    }

    /** @deprecated */
    @Deprecated
    public static String get(String key, String defaultValue) {
        return configManager.get(key, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static String get(String key, String defaultValue, ConfigContext context) {
        return configManager.get(key, defaultValue, context);
    }

    /** @deprecated */
    @Deprecated
    public static String getStringValue(String key) {
        return configManager.get(key);
    }

    /** @deprecated */
    @Deprecated
    public static String getStringValue(String key, String defaultValue) {
        return configManager.get(key, defaultValue);
    }

    private static void assertNotNull(String key, String value) {
        if (value == null) {
            throw new LionException("config [" + key + "] does not exist");
        }
    }

    /** @deprecated */
    @Deprecated
    public static byte getByteValue(String key) {
        String value = configManager.get(key);
        assertNotNull(key, value);
        return Byte.parseByte(value);
    }

    /** @deprecated */
    @Deprecated
    public static Byte getByte(String key) {
        return configManager.getByteValue(key);
    }

    /** @deprecated */
    @Deprecated
    public static byte getByteValue(String key, byte defaultValue) {
        return configManager.getByteValue(key, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static short getShortValue(String key) {
        String value = configManager.get(key);
        assertNotNull(key, value);
        return Short.parseShort(value);
    }

    /** @deprecated */
    @Deprecated
    public static Short getShort(String key) {
        return configManager.getShortValue(key);
    }

    /** @deprecated */
    @Deprecated
    public static short getShortValue(String key, short defaultValue) {
        return configManager.getShortValue(key, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static int getIntValue(String key) {
        String value = configManager.get(key);
        assertNotNull(key, value);
        return Integer.parseInt(value);
    }

    /** @deprecated */
    @Deprecated
    public static Integer getInt(String key) {
        return configManager.getIntValue(key);
    }

    /** @deprecated */
    @Deprecated
    public static int getIntValue(String key, int defaultValue) {
        return configManager.getIntValue(key, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static long getLongValue(String key) {
        String value = configManager.get(key);
        assertNotNull(key, value);
        return Long.parseLong(value);
    }

    /** @deprecated */
    @Deprecated
    public static Long getLong(String key) {
        return configManager.getLongValue(key);
    }

    /** @deprecated */
    @Deprecated
    public static long getLongValue(String key, long defaultValue) {
        return configManager.getLongValue(key, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static float getFloatValue(String key) {
        String value = configManager.get(key);
        assertNotNull(key, value);
        return Float.parseFloat(value);
    }

    /** @deprecated */
    @Deprecated
    public static Float getFloat(String key) {
        return configManager.getFloatValue(key);
    }

    /** @deprecated */
    @Deprecated
    public static float getFloatValue(String key, float defaultValue) {
        return configManager.getFloatValue(key, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static double getDoubleValue(String key) {
        String value = configManager.get(key);
        assertNotNull(key, value);
        return Double.parseDouble(value);
    }

    /** @deprecated */
    @Deprecated
    public static Double getDouble(String key) {
        return configManager.getDoubleValue(key);
    }

    /** @deprecated */
    @Deprecated
    public static double getDoubleValue(String key, double defaultValue) {
        return configManager.getDoubleValue(key, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static boolean getBooleanValue(String key) {
        String value = configManager.get(key);
        assertNotNull(key, value);
        return Boolean.parseBoolean(value);
    }

    /** @deprecated */
    @Deprecated
    public static Boolean getBoolean(String key) {
        return configManager.getBooleanValue(key);
    }

    /** @deprecated */
    @Deprecated
    public static boolean getBooleanValue(String key, boolean defaultValue) {
        return configManager.getBooleanValue(key, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static List<String> getList(String key) {
        return configManager.getList(key);
    }

    /** @deprecated */
    @Deprecated
    public static <T> List<T> getList(String key, Class<T> clazz) {
        return configManager.getList(key, clazz);
    }

    /** @deprecated */
    @Deprecated
    public static <T> List<T> getList(String key, Class<T> clazz, List<T> defaultValue) {
        return configManager.getList(key, clazz, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static Map<String, String> getMap(String key) {
        return configManager.getMap(key);
    }

    /** @deprecated */
    @Deprecated
    public static <T> Map<String, T> getMap(String key, Class<T> clazz) {
        return configManager.getMap(key, clazz);
    }

    /** @deprecated */
    @Deprecated
    public static <T> Map<String, T> getMap(String key, Class<T> clazz, Map<String, T> defaultValue) {
        return configManager.getMap(key, clazz, defaultValue);
    }

    /** @deprecated */
    @Deprecated
    public static <T> T getBean(String key, Class<T> clazz) {
        return configManager.getBean(key, clazz);
    }

    /** @deprecated */
    @Deprecated
    public static <T> T getBean(String key, Class<T> clazz, T defaultValue) {
        return configManager.getBean(key, clazz, defaultValue);
    }

    public static String getString(String appkey, String key) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.get(key, (String)null);
    }

    public static String getString(String appkey, String key, String defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.get(key, defaultValue);
    }

    public static Byte getByte(String appkey, String key) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getByteValue(key, (Byte)null);
    }

    public static Byte getByte(String appkey, String key, Byte defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getByteValue(key, defaultValue);
    }

    public static Short getShort(String appkey, String key) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getShortValue(key, (Short)null);
    }

    public static Short getShort(String appkey, String key, Short defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getShortValue(key, defaultValue);
    }

    public static Integer getInt(String appkey, String key) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getIntValue(key, (Integer)null);
    }

    public static Integer getInt(String appkey, String key, Integer defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getIntValue(key, defaultValue);
    }

    public static Long getLong(String appkey, String key) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getLongValue(key, (Long)null);
    }

    public static Long getLong(String appkey, String key, Long defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getLongValue(key, defaultValue);
    }

    public static Float getFloat(String appkey, String key) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getFloatValue(key, (Float)null);
    }

    public static Float getFloat(String appkey, String key, Float defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getFloatValue(key, defaultValue);
    }

    public static Double getDouble(String appkey, String key) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getDoubleValue(key, (Double)null);
    }

    public static Double getDouble(String appkey, String key, Double defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getDoubleValue(key, defaultValue);
    }

    public static Boolean getBoolean(String appkey, String key) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getBooleanValue(key, (Boolean)null);
    }

    public static Boolean getBoolean(String appkey, String key, Boolean defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getBooleanValue(key, defaultValue);
    }

    public static <T> List<T> getList(String appkey, String key, Class<T> clazz) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getList(key, clazz);
    }

    public static <T> List<T> getList(String appkey, String key, Class<T> clazz, List<T> defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getList(key, clazz, defaultValue);
    }

    public static <T> Map<String, T> getMap(String appkey, String key, Class<T> clazz) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getMap(key, clazz);
    }

    public static <T> Map<String, T> getMap(String appkey, String key, Class<T> clazz, Map<String, T> defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getMap(key, clazz, defaultValue);
    }

    public static <T> T getBean(String appkey, String key, Class<T> clazz) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getBean(key, clazz);
    }

    public static <T> T getBean(String appkey, String key, Class<T> clazz, T defaultValue) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, "default");
        return repository.getBean(key, clazz, defaultValue);
    }

    public static ConfigRepository getConfigRepository() {
        return getConfigRepository(Environment.getAppName(), "default");
    }

    public static ConfigRepository getConfigRepository(String appkey) {
        return getConfigRepository(appkey, "default");
    }

    public static ConfigRepository getConfigRepository(String appkey, String group) {
        return ConfigRepositoryManager.getInstance().getConfigRepository(appkey, group);
    }

    public static ConfigRepository getConfigRepository(String appkey, String userName, String password) {
        return getConfigRepository(appkey, "default", userName, password);
    }

    public static ConfigRepository getConfigRepository(String appkey, String group, String userName, String password) {
        return ConfigRepositoryManager.getInstance().getConfigRepository(appkey, group, userName, password);
    }

    /** @deprecated */
    public static void addConfigChangeListener(ConfigChange configChange) {
        configManager.addConfigListener(new ConfigListenerWrapper(configChange));
    }

    /** @deprecated */
    public static void removeConfigChangeListener(ConfigChange configChange) {
        configManager.removeConfigListener(new ConfigListenerWrapper(configChange));
    }

    public static void addConfigListener(ConfigListener configListener) {
        configManager.addConfigListener(configListener);
    }

    public static void addConfigListener(ConfigListener configListener, ExecutorService executor) {
        configManager.addConfigListener(configListener, executor);
    }

    /** @deprecated */
    @Deprecated
    public static void removeConfigListener(ConfigListener configListener) {
        configManager.removeConfigListener(configListener);
    }

    /** @deprecated */
    @Deprecated
    public static void addConfigListener(String key, ConfigListener configListener) {
        configManager.addConfigListener(key, configListener);
    }

    /** @deprecated */
    @Deprecated
    public static void addConfigListener(String key, ConfigListener configListener, ExecutorService executor) {
        configManager.addConfigListener(key, configListener, executor);
    }

    /** @deprecated */
    @Deprecated
    public static void removeConfigListener(String key, ConfigListener configListener) {
        configManager.removeConfigListener(key, configListener);
    }

    public static void addConfigListener(String appkey, String group, ConfigListener configListener) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, group);
        repository.addConfigListener(configListener);
    }

    public static void addConfigListener(String appkey, String group, ConfigListener configListener, ExecutorService executor) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, group);
        repository.addConfigListener(configListener, executor);
    }

    public static void removeConfigListener(String appkey, String group, ConfigListener configListener) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, group);
        repository.removeConfigListener(configListener);
    }

    public static void addConfigListener(String appkey, String group, String name, ConfigListener configListener) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, group);
        repository.addConfigListener(name, configListener);
    }

    public static void addConfigListener(String appkey, String group, String name, ConfigListener configListener, ExecutorService executor) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, group);
        repository.addConfigListener(name, configListener, executor);
    }

    public static void removeConfigListener(String appkey, String group, String name, ConfigListener configListener) {
        ConfigRepository repository = repositoryManager.getConfigRepository(appkey, group);
        repository.removeConfigListener(name, configListener);
    }

    public static void quit() {
        PollManager.getInstance().stop();
        FileConfigManagerFactory.stop();
    }

    public static FileConfigClient createFileConfigClient(String appkey) {
        return new FileConfigClient(appkey);
    }

    public static FileConfigClient createFileConfigClient() {
        String appkey = Environment.getAppName();
        Assert.notNull(appkey, "appkey of FileConfigClient should not be null");
        return new FileConfigClient(appkey);
    }
}

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值