springcloud gateway 如何获取post body请求参数

在Spring Cloud Gateway中,读取Get请求的参数并非一件很难的事,但是读取Post请求的请求体(body)也并非一件简单事。

项目开发有一段时间了,上次写了开头,忙其他事情就断更了。本次把解决方案贴出来供大家参考。不是原创,忘记从哪里找到的方案了,对不起原创作者。如果被发现侵权,我会删帖。

作用: 此过滤器主要作用是将请求body读取出来并存到exchange的自定义属性中,等待后续过滤器(ModifyRequestBodyFilter)处理
 * 使用: 使用本过滤器应该配合ModifyRequestBodyFilter一起使用,并且两个过滤器执行顺序必须CacheRequestBodyFilter在前
 *      ModifyRequestBodyFilter在后,否则后续业务将无法获得body中的参数

**
 *
 * 作用: 此过滤器主要作用是将请求body读取出来并存到exchange的自定义属性中,等待后续过滤器(ModifyRequestBodyFilter)处理
 * 使用: 使用本过滤器应该配合ModifyRequestBodyFilter一起使用,并且两个过滤器执行顺序必须CacheRequestBodyFilter在前
 *      ModifyRequestBodyFilter在后,否则后续业务将无法获得body中的参数
 * 
 */
@Slf4j
@Component
@Order(value = -100)
public class CacheRequestBodyFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 将 request body 中的内容复制一份,记录到 exchange 的一个自定义属性中
        Object cachedRequestBodyObject = exchange.getAttributeOrDefault(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY, null);
        // 如果已经缓存过,略过
        if (ObjectUtil.isNotNull(cachedRequestBodyObject)) {
            return chain.filter(exchange);
        }
        // 如果没有缓存过,获取字节数组存入 exchange 的自定义属性中
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .map(dataBuffer -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);
                    return bytes;
                }).defaultIfEmpty(new byte[0])
                .doOnNext(bytes -> exchange.getAttributes().put(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY, bytes))
                .then(chain.filter(exchange));
    }

自定义请求体过滤器
 * 主要作用:将被CacheRequestBodyFilter 读取过body的请求换成一个新的请求继续向下传递
 * 原因:每个请求body只能被读取一次,当body被将被CacheRequestBodyFilter读取后,后续业务将无法正常收到body,
 *      所以用一个新的请求继续,也因此,本过滤器执行顺序(order)必须在CacheRequestBodyFilter之后
 * 扩展:这里也可以对原body进行修改,但目前不需要修改,只需要将原body从exchange自定义属性中取出来放到新请求中即可

/**
 * 自定义请求体过滤器
 * 主要作用:将被CacheRequestBodyFilter 读取过body的请求换成一个新的请求继续向下传递
 * 原因:每个请求body只能被读取一次,当body被将被CacheRequestBodyFilter读取后,后续业务将无法正常收到body,
 *      所以用一个新的请求继续,也因此,本过滤器执行顺序(order)必须在CacheRequestBodyFilter之后
 * 扩展:这里也可以对原body进行修改,但目前不需要修改,只需要将原body从exchange自定义属性中取出来放到新请求中即可
 * 
 */
@Slf4j
@Component
@Order(value = -90)
public class ModifyRequestBodyFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 尝试从 exchange 的自定义属性中取出缓存到的 body
        Object cachedRequestBodyObject = exchange.getAttributeOrDefault(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY, null);
        if (ObjectUtil.isNotNull(cachedRequestBodyObject)) {
            byte[] body = (byte[]) cachedRequestBodyObject;
            DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
            ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux<DataBuffer> getBody() {
                    if (body.length > 0) {
                        return Flux.just(dataBufferFactory.wrap(body));
                    }
                    return Flux.empty();
                }
            };
            return chain.filter(exchange.mutate().request(decorator).build());
        }
        // 为空,说明已经读过,或者 request body 原本即为空,不做操作,传递到下一个过滤器链
        return chain.filter(exchange);
    }

}
public class FilterConstant {
    public static final String CACHED_REQUEST_BODY_OBJECT_KEY = "CACHED_REQUEST_BODY_OBJECT_KEY";
}

此过滤器为限流功能,集成sentinal组件

@Order(-1)
@Component
@Slf4j
public class SentinelFilter implements GlobalFilter {

    @Resource
    private RuleDataLoad ruleDataLoad;

    public final static String X_ACCESS_TOKEN = "X-Access-Token";

    @Value("${jwt.redis-token-prefix}")
    String tokenPrefix;

    private String GET = "get";

    private String POST = "post";

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        URI requestURI = request.getURI();
        String path = requestURI.getPath();
        boolean flowRuleFlag = ruleDataLoad.getFlowRuleBySource(path);
        boolean result = true;
        // 命中限流规则
        if (flowRuleFlag) {
            result = limitResult(exchange, path);
        }
        // 默认限流开关打开
        else if (ruleDataLoad.getDefaultLimitOpen()) {
            result = limitResult(exchange, "default");
        }
        if (!result) {
            // 发送响应并结束请求
            return setServerHttpResponse(exchange);
        } else {
            return chain.filter(exchange);
        }
    }

    /**
     * 限流提示
     *
     * @param exchange
     * @return
     */
    public Mono setServerHttpResponse(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        // 语言标识zh_Hant(中文)、en(英文),无时默认中文
        String lang = request.getHeaders().getFirst("lang");
        String message;
        if(StringUtils.isBlank(lang) || "zh_Hant".equals(lang)){
            message = "网络繁忙,请稍后再试";
        } else {
            message = "Network is busy,please try again later";
        }
        ServerHttpResponse response = exchange.getResponse();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", 506);
        jsonObject.put("success", false);
        jsonObject.put("message", message);
        byte[] responseBody = jsonObject.toJSONString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(responseBody);
        // 设置响应状态码、头部信息等
        response.getHeaders().setContentType(MediaType.valueOf("application/json;charset=utf-8"));
        response.getHeaders().setContentLength(responseBody.length);
        return response.writeWith(Mono.just(buffer));
    }

    private boolean limitResult(ServerWebExchange exchange, String path) {
        if (!this.limitHandler(exchange, path)) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * 限流
     *
     * @param exchange
     * @return
     */
    public boolean limitHandler(ServerWebExchange exchange, String resourceName) {
        Entry entry = null;
        try {
            entry = SphU.entry(resourceName);
            return true;
        } catch (BlockException e) {
            this.log(exchange);
            return false;
        } catch (Throwable t) {
            Tracer.trace(t);
            return false;
        } finally {
            // 务必保证 exit,务必保证每个 entry 与 exit 配对
            if (entry != null) {
                entry.exit();
            }
        }
    }

    /**
     * 打印日志
     *
     * @param exchange
     */
    public void log(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        String userId = null;
        try {
            String token = request.getHeaders().getFirst(X_ACCESS_TOKEN);
            if (token != null) {
                String tokenKey = tokenPrefix + MD5Utils.md5Hex(token.getBytes(StandardCharsets.UTF_8));
                Object userInfo = stringRedisTemplate.opsForValue().get(tokenKey);
                if (userInfo != null) {
                    userId = JSONObject.from(userInfo).getString("userId");
                }
            }
        } catch (Exception e) {
            log.info("获取用户id失败", e);
        }
        try {
            URI requestURI = request.getURI();
            String path = requestURI.getPath();
            String ip = getIpAddress(request);
            String method = request.getMethodValue();

            HttpHeaders httpHeaders = request.getHeaders();
            Set<Map.Entry<String, List<String>>> set = httpHeaders.entrySet();
            Map<String, List<String>> headerMap = new HashMap<>();
            for (Iterator<Map.Entry<String, List<String>>> iterator = set.iterator(); iterator.hasNext(); ) {
                Map.Entry<String, List<String>> entry = iterator.next();
                String key = entry.getKey();
                List<String> valueList = entry.getValue();
                headerMap.put(key, valueList);
            }

            StringBuilder stringBuilder = new StringBuilder();
            if (this.GET.equalsIgnoreCase(method)) {
                stringBuilder.append(request.getQueryParams());
                log.info("触发限流={},ip={},userid={},header={},get请求参数={}",
                        path, ip, userId, JSONObject.toJSONString(headerMap), stringBuilder);
            } else if (this.POST.equalsIgnoreCase(method)) {
                String bodystr = "";
                Object cachedRequestBodyObject = exchange.getAttributes().get(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY);
                if (cachedRequestBodyObject != null) {
                    byte[] body = (byte[]) cachedRequestBodyObject;
                    String string = new String(body);
                    bodystr = string;
                }
                Map requestBodyMap = JSONObject.parseObject(bodystr, Map.class);
                log.info("触发限流={},ip={},userid={},header={},body请求参数={}",
                        path, ip, userId, headerMap, requestBodyMap);
            } else {
                log.info("触发限流={},ip={},userid={},header={}",
                        path, ip, userId, headerMap);
            }
        } catch (Exception e) {
            log.error("log限流日志错误信息{}", e.getMessage(), e);
        }
    }

    /**
     * get ip address
     *
     * @param request
     * @return
     */
    private String getIpAddress(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ip = headers.getFirst("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if (ip.indexOf(",") != -1) {
                ip = ip.split(",")[0];
            }
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            InetSocketAddress socketAddress = request.getRemoteAddress();
            if(Objects.nonNull(socketAddress) && Objects.nonNull(socketAddress.getAddress())){
                ip = socketAddress.getAddress().getHostAddress();
            }
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}

写到此处,我把限流的集成罗列出来,供大家学习

POM文件

<sentinel-core.version>1.8.5</sentinel-core.version>
<dynamic-config-spring-boot-starter.version>0.1.1.RELEASE</dynamic-config-spring-boot-starter.version>
<dependency>
    <groupId>com.purgeteam</groupId>
    <artifactId>dynamic-config-spring-boot-starter</artifactId>
    <version>${dynamic-config-spring-boot-starter.version}</version>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>${sentinel-core.version}</version>
</dependency>

启动类添加@EnableDynamicConfigEvent

定义拦截器


@Slf4j
@Component
public class SentinelInterceptor implements HandlerInterceptor {

    @Resource
    private RuleDataLoad ruleDataLoad;

    @Resource
    private TokenService tokenService;

    private String GET = "get";

    private String POST = "post";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        String path = request.getRequestURI();
        boolean flowRuleFlag = ruleDataLoad.getFlowRuleBySource(path);
        // 命中限流规则
        if (flowRuleFlag) {
            return limitResult(request, response, path);
        }
        // 默认限流开关打开
        else if (ruleDataLoad.getDefaultLimitOpen()) {
            // 默认限流规则
            return limitResult(request, response, "default");
        } else {
            return true;
        }
    }

    /**
     * 限流处理结果
     *
     * @param request
     * @param response
     * @param path
     * @return
     * @throws IOException
     */
    public boolean limitResult(HttpServletRequest request, HttpServletResponse response, String path) throws IOException {
        if (!this.limitHandler(request, path)) {
            this.print(response);
            return false;
        } else {
            return true;
        }
    }

    /**
     * 打印提示信息
     *
     * @param response
     * @throws IOException
     */
    public void print(HttpServletResponse response) throws IOException {
        response.setContentType("application/json");
        PrintWriter out = response.getWriter();
        GenericResult result = new GenericResult();
        result.setCode(500);
        result.setSuccess(false);
        result.setValue("system busy,please try later!");
        out.print(JSONObject.toJSONString(result));
        out.close();
    }

    /**
     * 打印日志
     *
     * @param request
     */
    public void log(HttpServletRequest request) {
        try {
            String userId = null;
            if (tokenService != null) {
                try {
                    userId = tokenService.getUserId();
                } catch (Exception e) {
                    //log.info("获取用户id失败", e);
                }
            }
            String path = request.getRequestURI();
            String ip = getIpAdrress(request);
            String method = request.getMethod();
            Enumeration<String> headerNames = request.getHeaderNames();
            Map headers = new HashMap<>();
            while (headerNames.hasMoreElements()) {
                String headerName = headerNames.nextElement();
                String headerValue = request.getHeader(headerName);
                headers.put(headerName, headerValue);
            }

            BufferedReader reader = request.getReader();
            StringBuilder stringBuilder = new StringBuilder();
            if (this.GET.equalsIgnoreCase(method)) {
                stringBuilder.append(request.getQueryString());
                log.info("触发限流={},ip={},userid={},header={},get请求参数={}",
                        path, ip, userId, headers, stringBuilder);
            } else if (this.POST.equalsIgnoreCase(method)) {
                String line;
                while ((line = reader.readLine()) != null) {
                    stringBuilder.append(line);
                }
                String body = stringBuilder.toString();
                Map requestBodyMap = new Gson().fromJson(body, Map.class);
                log.info("触发限流={},ip={},userid={},header={},body请求参数={}",
                        path, ip, userId, headers, requestBodyMap);
            } else {
                log.info("触发限流={},ip={},userid={},header={}",
                        path, ip, userId, headers);
            }
        } catch (Exception e) {
            log.error("log限流日志错误信息{}", e.getMessage(), e);
        }
    }

    /**
     * 限流
     *
     * @param request
     * @return
     */
    public boolean limitHandler(HttpServletRequest request, String resourceName) {
        Entry entry = null;
        boolean flag = false;
        try {
            entry = SphU.entry(resourceName);
            flag = true;
        } catch (BlockException e) {
            this.log(request);
            flag = false;
        } catch (Throwable t) {
            Tracer.trace(t);
            flag = false;
        } finally {
            // 务必保证 exit,务必保证每个 entry 与 exit 配对
            if (entry != null) {
                entry.exit();
            }
            return flag;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

    public String getIpAdrress(HttpServletRequest request) {
        String Xip = request.getHeader("X-Real-IP");
        String XFor = request.getHeader("X-Forwarded-For");
        if (StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)) {
            //多次反向代理后会有多个ip值,第一个ip才是真实ip
            int index = XFor.indexOf(",");
            if (index != -1) {
                return XFor.substring(0, index);
            } else {
                return XFor;
            }
        }
        XFor = Xip;
        if (StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)) {
            return XFor;
        }
        if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
            XFor = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
            XFor = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
            XFor = request.getHeader("HTTP_CLIENT_IP");
        }
        if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
            XFor = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
            XFor = request.getRemoteAddr();
        }
        return XFor;
    }

}

注册拦截器

@Configuration
public class SentinelConfigure extends WebMvcConfigurationSupport {

    @Resource
    private SentinelInterceptor sentinalInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(sentinalInterceptor).addPathPatterns("/**");
        super.addInterceptors(registry);
    }

    @Override
    protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.defaultContentType(MediaType.APPLICATION_JSON);
    }
}
@Component
@Order(1)
@Slf4j
public class RuleDataLoad {

    @Resource
    private ApiRule apiRule;

    @PostConstruct
    public void init() {
        initDegradeRatioRule();
        initFlowRules();
    }

    public boolean getDefaultLimitOpen() {
        try {
            return apiRule.getDefaultLimitOpen();
        } catch (Exception e) {
            log.error("获取默认限流开关异常{}", e.getMessage(), e);
        }
        return false;
    }

    public boolean getFlowRuleBySource(String urlKey) {
        try {
            List<ApiFlowRule> flowList = apiRule.getFlow();
            for (int i = 0; flowList != null && i < flowList.size(); i++) {
                ApiFlowRule flowRule = flowList.get(i);
                String resource = flowRule.getResource();
                if (urlKey.equals(resource)) {
                    return true;
                }
            }
        } catch (Exception e) {
            log.error("getFlowRuleBySource异常{}", e.getMessage(), e);
        }
        return false;
    }

    public boolean getDegradeRuleBySource(String urlKey) {
        try {
            List<ApiDegradeRule> degradeList = apiRule.getDegrade();
            for (int i = 0; degradeList != null && i < degradeList.size(); i++) {
                ApiDegradeRule degradeRule = degradeList.get(i);
                String resource = degradeRule.getResource();
                if (urlKey.equals(resource)) {
                    return true;
                }
            }
        } catch (Exception e) {
            log.error("getDegradeRuleBySource异常{}", e.getMessage(), e);
        }
        return false;
    }

    public void initDegradeRatioRule() {
        try {
            List<DegradeRule> rules = new ArrayList<>();
            List<ApiDegradeRule> apiRules = apiRule.getDegrade();
            log.info("initDegradeRatioRule 加载熔断配置={}", JSON.toJSONString(apiRules));
            for (int i = 0; apiRules != null && i < apiRules.size(); i++) {
                ApiDegradeRule source = apiRules.get(i);
                DegradeRule degradeRule = new DegradeRule();
                degradeRule.setResource(source.getResource());
                degradeRule.setCount(source.getCount());
                degradeRule.setGrade(source.getGrade());
                degradeRule.setSlowRatioThreshold(source.getSlowRatioThreshold());
                degradeRule.setMinRequestAmount(source.getMinRequestAmount());
                degradeRule.setTimeWindow(source.getTimeWindow());
                rules.add(degradeRule);
            }
            DegradeRuleManager.loadRules(rules);
        } catch (Exception e) {
            log.error("initDegradeRatioRule error loading rules错误信息{}", e.getMessage(), e);
        }
    }

    public void initFlowRules() {
        try {
            List<FlowRule> rules = new ArrayList<>();
            List<ApiFlowRule> apiRules = apiRule.getFlow();
            log.info("initFlowRules加载限流配置={}", JSON.toJSONString(apiRules));
            for (int i = 0; apiRules != null && i < apiRules.size(); i++) {
                ApiFlowRule source = apiRules.get(i);
                FlowRule flowRule = new FlowRule();
                flowRule.setResource(source.getResource());
                flowRule.setCount(source.getCount());
                flowRule.setGrade(source.getGrade());
                rules.add(flowRule);
            }
            FlowRuleManager.loadRules(rules);
        } catch (Exception e) {
            log.error("initFlowRules error loading rules错误信息{}", e.getMessage(), e);
        }
    }
}

动态刷新nacos配置,如果限流规则有变,此处更新java内存

@Slf4j
@Component
public class RuleListener implements ApplicationListener<ActionConfigEvent> {

    @Resource
    private RuleDataLoad dataLoad;

    private String ruleName = "sentinel.rule";

    @Override
    public void onApplicationEvent(ActionConfigEvent event) {
        try {
            Map<String, HashMap> map = event.getPropertyMap();
            if(map != null){
                Set<String> keySet = map.keySet();
                keySet.stream().anyMatch(s->{
                    if(s.startsWith(ruleName)){
                        dataLoad.init();
                        return true;
                    }
                    return false;
                });
            }
        } catch (Exception e) {
            log.error("onApplicationEvent错误信息{}", e.getMessage(), e);
        }
    }
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
要在Spring Cloud Gateway中添加POST请求参数,请按照以下步骤操作: 1. 在Spring Cloud Gateway的配置文件中,添加一个过滤器来修改请求。例如,你可以使用以下代码来创建一个过滤器类: ``` @Component public class ModifyRequestBodyGatewayFilterFactory extends AbstractGatewayFilterFactory<ModifyRequestBodyGatewayFilterFactory.Config> { public ModifyRequestBodyGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); Flux<DataBuffer> body = request.getBody(); MediaType mediaType = request.getHeaders().getContentType(); if (MediaType.APPLICATION_JSON.equals(mediaType)) { // 修改请求 String modifiedBody = "{\"name\":\"value\"}"; DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(modifiedBody.getBytes()); return chain.filter(exchange.mutate().request(request.mutate().body(body)).build()); } return chain.filter(exchange); }; } public static class Config { // 配置参数 } } ``` 2. 在Spring Cloud Gateway的配置文件中,将过滤器添加到路由中。例如,你可以使用以下代码来配置一个路由: ``` spring: cloud: gateway: routes: - id: my-route uri: http://example.com predicates: - Path=/my/path filters: - ModifyRequestBody=name=value ``` 在这个例子中,`ModifyRequestBody`是上面创建的过滤器类的名称,`name=value`是要添加到POST请求中的参数。 注意,这个例子只修改了JSON请求。如果你需要修改其他类型的请求,请相应地更改代码。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值