默认的GlobalFilter学习

Spring-Cloud-Gateway源码系列学习

版本 v2.2.6.RELEASE

默认的GlobalFilter都有哪些

在这里插入图片描述

根据debug可以得出以下order顺序表格,一个请求都会经过这些GlobalFilter,这些 GlobalFilter 会在 FilteringWebHandler 通过 GatewayFilterAdapter 适配成GatewayFilter

GlobalFilter类order
RemoveCachedBodyFilterInteger.MIN_VALUE
AdaptCachedBodyGlobalFilterInteger.MIN_VALUE + 1000
NettyWriteResponseFilter-1
ForwardPathFilter0
GatewayMetricsFilter0
RouteToRequestUrlFilter10000
LoadBalancerClientFilter10100
WebsocketRoutingFilterInteger.MAX_VALUE
NettyRoutingFilterInteger.MAX_VALUE
ForwardRoutingFilterInteger.MAX_VALUE

tip:其中最应该关注的分别是NettyWriteResponseFilter和NettyRoutingFilter,NettyRoutingFilter负责发送请求到route目标网址(需要合并),而NettyWriteResponseFilter负责把响应结果发回客户端(根据是否是流数据,分别处理)

RemoveCachedBodyFilter源码分析

在流结束之前移除exchange上下文里的CACHED_REQUEST_BODY_ATTR

public class RemoveCachedBodyFilter implements GlobalFilter, Ordered {

	private static final Log log = LogFactory.getLog(RemoveCachedBodyFilter.class);

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //注意doFinally,相当于try finally,在流结束之前做的,也就是最后才移除CACHED_REQUEST_BODY_ATTR
		return chain.filter(exchange).doFinally(s -> {
            //倒数第一执行
            //移除对request body的缓存
			Object attribute = exchange.getAttributes().remove(CACHED_REQUEST_BODY_ATTR);
			if (attribute != null && attribute instanceof PooledDataBuffer) {
				PooledDataBuffer dataBuffer = (PooledDataBuffer) attribute;
				//释放堆外内存
				if (dataBuffer.isAllocated()) {
					if (log.isTraceEnabled()) {
						log.trace("releasing cached body in exchange attribute");
					}
					dataBuffer.release();
				}
			}
		});
	}

    //在Ordered接口里面 int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;最高优先级
	@Override
	public int getOrder() {
		return HIGHEST_PRECEDENCE;
	}

}

AdaptCachedBodyGlobalFilter源码分析

这一步主要是判断需不需要进行RequestBody的缓存,如果有缓存则对exchange进行一些调整(替换成缓存的)

public class AdaptCachedBodyGlobalFilter
		implements GlobalFilter, Ordered, ApplicationListener<EnableBodyCachingEvent> {

    //记录需要缓存的 routeId
	private ConcurrentMap<String, Boolean> routesToCache = new ConcurrentHashMap<>();

    //通过事件驱动 更新需要缓存的 routeId
	@Override
	public void onApplicationEvent(EnableBodyCachingEvent event) {
		this.routesToCache.putIfAbsent(event.getRouteId(), true);
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		// the cached ServerHttpRequest is used when the ServerWebExchange can not be
		// mutated, for example, during a predicate where the body is read, but still
		// needs to be cached.
        //如果RequestBody和Request都缓存的话,将会获得到,@see ServerWebExchangeUtils#cacheRequestBodyAndRequest return执行方法的第二个参数
        /**
		 * @see RetryGatewayFilterFactory#apply(java.lang.String, reactor.retry.Repeat, reactor.retry.Retry)
		 * @param event
		 */
		ServerHttpRequest cachedRequest = exchange
				.getAttributeOrDefault(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR, null);
		if (cachedRequest != null) {
            //移除
			exchange.getAttributes().remove(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR);
            //替换exchange 的 request 为 缓存的 cachedRequest
			return chain.filter(exchange.mutate().request(cachedRequest).build());
		}

		//获取 CACHED_REQUEST_BODY_ATTR
		DataBuffer body = exchange.getAttributeOrDefault(CACHED_REQUEST_BODY_ATTR, null);
        //获取 Route
		Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);

        //如果 body 不为空 或者 Route不需要缓存 直接开始下一个filter
		if (body != null || !this.routesToCache.containsKey(route.getId())) {
			return chain.filter(exchange);
		}
		
        //ServerWebExchangeUtils.cacheRequestBody就是缓存RequestBody,但不会缓存整个Request,也就是不会保存CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR
        //进行缓存
		return ServerWebExchangeUtils.cacheRequestBody(exchange, (serverHttpRequest) -> {
			// don't mutate and build if same request object
            // 如果相等则不需要修改exchange的request
			if (serverHttpRequest == exchange.getRequest()) {
				return chain.filter(exchange);
			}
			return chain.filter(exchange.mutate().request(serverHttpRequest).build());
		});
	}

    //Integer.MIN_VALUE + 1000
	@Override
	public int getOrder() {
		return Ordered.HIGHEST_PRECEDENCE + 1000;
	}

}

NettyWriteResponseFilter源码分析

跟NettyRoutingFilter一对的,NettyRoutingFilter负责请求目标地址返回响应结果,NettyWriteResponseFilter负责把响应结果发回给客户端

public class NettyWriteResponseFilter implements GlobalFilter, Ordered {

	/**
	 * Order for write response filter.
	 */
	public static final int WRITE_RESPONSE_FILTER_ORDER = -1;

	private static final Log log = LogFactory.getLog(NettyWriteResponseFilter.class);

	private final List<MediaType> streamingMediaTypes;

	public NettyWriteResponseFilter(List<MediaType> streamingMediaTypes) {
		this.streamingMediaTypes = streamingMediaTypes;
	}

    // -1
	@Override
	public int getOrder() {
		return WRITE_RESPONSE_FILTER_ORDER;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		// NOTICE: nothing in "pre" filter stage as CLIENT_RESPONSE_CONN_ATTR is not added
		// until the NettyRoutingFilter is run
		// @formatter:off
		return chain.filter(exchange)
				.doOnError(throwable -> cleanup(exchange))
				.then(Mono.defer(() -> {
                    //倒数第二执行这里,注意.then
                    //获得NettyRoutingFilter返回的Response
					Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);
					if (connection == null) {
                        //如果NettyRoutingFilter 返回的 connection为null,直接返回空
						return Mono.empty();
					}
					if (log.isTraceEnabled()) {
						log.trace("NettyWriteResponseFilter start inbound: "
								+ connection.channel().id().asShortText() + ", outbound: "
								+ exchange.getLogPrefix());
					}
                    // 
					ServerHttpResponse response = exchange.getResponse();

					// TODO: needed?
                    //获得connection的inbound数据流,并转成DataBuffer类型元素
					final Flux<DataBuffer> body = connection
							.inbound()
							.receive()
							.retain()
							.map(byteBuf -> wrap(byteBuf, response));

                    //获取响应内容类型
					MediaType contentType = null;
					try {
						contentType = response.getHeaders().getContentType();
					}
					catch (Exception e) {
						if (log.isTraceEnabled()) {
							log.trace("invalid media type", e);
						}
					}
                    //针对流类型按流和普通的分表处理
					return (isStreamingMediaType(contentType)
							? response.writeAndFlushWith(body.map(Flux::just))
							: response.writeWith(body));
				})).doOnCancel(() -> cleanup(exchange));
		// @formatter:on
	}

    //把netty请求的ByteBuf转成DataBuffer
	protected DataBuffer wrap(ByteBuf byteBuf, ServerHttpResponse response) {
		DataBufferFactory bufferFactory = response.bufferFactory();
		if (bufferFactory instanceof NettyDataBufferFactory) {
			NettyDataBufferFactory factory = (NettyDataBufferFactory) bufferFactory;
			return factory.wrap(byteBuf);
		}
		// MockServerHttpResponse creates these
		else if (bufferFactory instanceof DefaultDataBufferFactory) {
			DataBuffer buffer = ((DefaultDataBufferFactory) bufferFactory)
					.allocateBuffer(byteBuf.readableBytes());
			buffer.write(byteBuf.nioBuffer());
			byteBuf.release();
			return buffer;
		}
		throw new IllegalArgumentException(
				"Unkown DataBufferFactory type " + bufferFactory.getClass());
	}

	private void cleanup(ServerWebExchange exchange) {
		Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);
		if (connection != null) {
			connection.dispose();
		}
	}

	// TODO: use framework if possible
	// TODO: port to WebClientWriteResponseFilter
	private boolean isStreamingMediaType(@Nullable MediaType contentType) {
		return (contentType != null && this.streamingMediaTypes.stream()
				.anyMatch(contentType::isCompatibleWith));
	}

}

ForwardPathFilter源码分析

判断是不是forward,是则替换request的path

public class ForwardPathFilter implements GlobalFilter, Ordered {

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取Route
		Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
        //获取Uri
		URI routeUri = route.getUri();
        //获取协议,如https
		String scheme = routeUri.getScheme();
        //如果已经处理了(可以看看NettyRoutingFilter#filter)或者不是转发的
		if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) {
            //下个filter
			return chain.filter(exchange);
		}
        //如果forward则替换path
		exchange = exchange.mutate()
				.request(exchange.getRequest().mutate().path(routeUri.getPath()).build())
				.build();
		return chain.filter(exchange);
	}

	@Override
	public int getOrder() {
		return 0;
	}

}

RouteToRequestUrlFilter源码分析

该类主要是拼接目标uri,例如 request = http://localhost:8080/111, route.uri = https://www.baidu.com,则mergeuri = https://www.baidu.com/111

public class RouteToRequestUrlFilter implements GlobalFilter, Ordered {

	/**
	 * Order of Route to URL.
	 */
	public static final int ROUTE_TO_URL_FILTER_ORDER = 10000;

	private static final Log log = LogFactory.getLog(RouteToRequestUrlFilter.class);

	private static final String SCHEME_REGEX = "[a-zA-Z]([a-zA-Z]|\\d|\\+|\\.|-)*:.*";
	static final Pattern schemePattern = Pattern.compile(SCHEME_REGEX);

	/* for testing */
	static boolean hasAnotherScheme(URI uri) {
		return schemePattern.matcher(uri.getSchemeSpecificPart()).matches()
				&& uri.getHost() == null && uri.getRawPath() == null;
	}

	@Override
	public int getOrder() {
		return ROUTE_TO_URL_FILTER_ORDER;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取Route
		Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
		if (route == null) {
			return chain.filter(exchange);
		}
		log.trace("RouteToRequestUrlFilter start");
        //获取请求uri
		URI uri = exchange.getRequest().getURI();
		boolean encoded = containsEncodedParts(uri);
        //获取目标uri
		URI routeUri = route.getUri();

        //如果是其他协议
		if (hasAnotherScheme(routeUri)) {
			// this is a special url, save scheme to special attribute
			// replace routeUri with schemeSpecificPart
			exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR,
					routeUri.getScheme());
			routeUri = URI.create(routeUri.getSchemeSpecificPart());
		}
		
        
		if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
			// Load balanced URIs should always have a host. If the host is null it is
			// most
			// likely because the host name was invalid (for example included an
			// underscore)
			throw new IllegalStateException("Invalid host: " + routeUri.toString());
		}

        //拼接 目标 Uri
		URI mergedUrl = UriComponentsBuilder.fromUri(uri)
				// .uri(routeUri)
				.scheme(routeUri.getScheme()).host(routeUri.getHost())
				.port(routeUri.getPort()).build(encoded).toUri();
        //存进GATEWAY_REQUEST_URL_ATTR
		exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);
        //下一个filter
		return chain.filter(exchange);
	}

}

NettyRoutingFilter

真正发送请求去目标地址,拿回响应结果

public class NettyRoutingFilter implements GlobalFilter, Ordered {

	private static final Log log = LogFactory.getLog(NettyRoutingFilter.class);

	private final HttpClient httpClient;

	private final ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider;

	private final HttpClientProperties properties;

	// do not use this headersFilters directly, use getHeadersFilters() instead.
	private volatile List<HttpHeadersFilter> headersFilters;

	public NettyRoutingFilter(HttpClient httpClient,
			ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider,
			HttpClientProperties properties) {
		this.httpClient = httpClient;
		this.headersFiltersProvider = headersFiltersProvider;
		this.properties = properties;
	}

	public List<HttpHeadersFilter> getHeadersFilters() {
		if (headersFilters == null) {
			headersFilters = headersFiltersProvider.getIfAvailable();
		}
		return headersFilters;
	}

	@Override
	public int getOrder() {
		return Ordered.LOWEST_PRECEDENCE;
	}

	@Override
	@SuppressWarnings("Duplicates")
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取RouteToRequestUrlFilter合并好的uri
		URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

        //获取协议
		String scheme = requestUrl.getScheme();
        //只能处理http/https协议的,如果有其他filter处理了这个请求(isAlreadyRouted)则不做处理
		if (isAlreadyRouted(exchange)
				|| (!"http".equals(scheme) && !"https".equals(scheme))) {
			return chain.filter(exchange);
		}
        //设置这个请求为已处理
		setAlreadyRouted(exchange);

        
		ServerHttpRequest request = exchange.getRequest();

        //获取请求 method (GET/POST...)
		final HttpMethod method = HttpMethod.valueOf(request.getMethodValue());
        
		final String url = requestUrl.toASCIIString();

		HttpHeaders filtered = filterRequest(getHeadersFilters(), exchange);
		
        //Request Header
		final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
		filtered.forEach(httpHeaders::set);

		boolean preserveHost = exchange
				.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false);
		Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);

        //创建请求流
		Flux<HttpClientResponse> responseFlux = getHttpClient(route, exchange)
				.headers(headers -> {
					headers.add(httpHeaders);
					// Will either be set below, or later by Netty
					headers.remove(HttpHeaders.HOST);
					if (preserveHost) {
						String host = request.getHeaders().getFirst(HttpHeaders.HOST);
						headers.add(HttpHeaders.HOST, host);
					}
				}).request(method).uri(url).send((req, nettyOutbound) -> {
					if (log.isTraceEnabled()) {
						nettyOutbound
								.withConnection(connection -> log.trace("outbound route: "
										+ connection.channel().id().asShortText()
										+ ", inbound: " + exchange.getLogPrefix()));
					}
					return nettyOutbound.send(request.getBody().map(this::getByteBuf));
				}).responseConnection((res, connection) -> {

					// Defer committing the response until all route filters have run
					// Put client response as ServerWebExchange attribute and write
					// response later NettyWriteResponseFilter
            		//保存响应信息到exchange上下文,流到NettyWriteResponseFilter的.then把响应结果发回客户端
            		//把响应结果放到CLIENT_RESPONSE_ATTR
					exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
            		//把connection放到CLIENT_RESPONSE_CONN_ATTR
					exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection);

					ServerHttpResponse response = exchange.getResponse();
					// put headers and status so filters can modify the response
					HttpHeaders headers = new HttpHeaders();

					res.responseHeaders().forEach(
							entry -> headers.add(entry.getKey(), entry.getValue()));

            		//获取数据类型,并保存到ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR
					String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE);
					if (StringUtils.hasLength(contentTypeValue)) {
						exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR,
								contentTypeValue);
					}

            		//把请求目标地址返回的status设置到exchange.getResponse
					setResponseStatus(res, response);

					//确保Response的HttpHeadersFilter在请求响应后执行
					HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter(
							getHeadersFilters(), headers, exchange, Type.RESPONSE);

					if (!filteredResponseHeaders
							.containsKey(HttpHeaders.TRANSFER_ENCODING)
							&& filteredResponseHeaders
									.containsKey(HttpHeaders.CONTENT_LENGTH)) {
						// It is not valid to have both the transfer-encoding header and
						// the content-length header.
						// Remove the transfer-encoding header in the response if the
						// content-length header is present.
						response.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING);
					}

					exchange.getAttributes().put(CLIENT_RESPONSE_HEADER_NAMES,
							filteredResponseHeaders.keySet());

					response.getHeaders().putAll(filteredResponseHeaders);

					return Mono.just(res);
				});

		Duration responseTimeout = getResponseTimeout(route);
		if (responseTimeout != null) {
            //发起请求
			responseFlux = responseFlux
                	//设置响应超时时间
					.timeout(responseTimeout, Mono.error(new TimeoutException(
							"Response took longer than timeout: " + responseTimeout)))
                	//传播一个超时异常
					.onErrorMap(TimeoutException.class,
							th -> new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT,
									th.getMessage(), th));
		}

		return responseFlux.then(chain.filter(exchange));
	}
	
    //把DataBuffer转成ByteBuf,用于netty请求目标地址
	protected ByteBuf getByteBuf(DataBuffer dataBuffer) {
		if (dataBuffer instanceof NettyDataBuffer) {
			NettyDataBuffer buffer = (NettyDataBuffer) dataBuffer;
			return buffer.getNativeBuffer();
		}
		// MockServerHttpResponse creates these
		else if (dataBuffer instanceof DefaultDataBuffer) {
			DefaultDataBuffer buffer = (DefaultDataBuffer) dataBuffer;
			return Unpooled.wrappedBuffer(buffer.getNativeBuffer());
		}
		throw new IllegalArgumentException(
				"Unable to handle DataBuffer of type " + dataBuffer.getClass());
	}

    //把请求目标地址返回的status设置到exchange.getResponse
	private void setResponseStatus(HttpClientResponse clientResponse,
			ServerHttpResponse response) {
		HttpStatus status = HttpStatus.resolve(clientResponse.status().code());
		if (status != null) {
			response.setStatusCode(status);
		}
		else {
			while (response instanceof ServerHttpResponseDecorator) {
				response = ((ServerHttpResponseDecorator) response).getDelegate();
			}
			if (response instanceof AbstractServerHttpResponse) {
				((AbstractServerHttpResponse) response)
						.setStatusCodeValue(clientResponse.status().code());
			}
			else {
				// TODO: log warning here, not throw error?
				throw new IllegalStateException("Unable to set status code "
						+ clientResponse.status().code() + " on response of type "
						+ response.getClass().getName());
			}
		}
	}

	/**
	 * Creates a new HttpClient with per route timeout configuration. Sub-classes that
	 * override, should call super.getHttpClient() if they want to honor the per route
	 * timeout configuration.
	 * @param route the current route.
	 * @param exchange the current ServerWebExchange.
	 * @param chain the current GatewayFilterChain.
	 * @return
	 */
    //创建一个基于Netty的HttpClient
	protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) {
		Object connectTimeoutAttr = route.getMetadata().get(CONNECT_TIMEOUT_ATTR);
		if (connectTimeoutAttr != null) {
			Integer connectTimeout = getInteger(connectTimeoutAttr);
			return this.httpClient.tcpConfiguration((tcpClient) -> tcpClient
					.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout));
		}
		return httpClient;
	}

    //根据route的Metadata的CONNECT_TIMEOUT_ATTR获取连接超时时间
	static Integer getInteger(Object connectTimeoutAttr) {
		Integer connectTimeout;
		if (connectTimeoutAttr instanceof Integer) {
			connectTimeout = (Integer) connectTimeoutAttr;
		}
		else {
			connectTimeout = Integer.parseInt(connectTimeoutAttr.toString());
		}
		return connectTimeout;
	}

    //根据route获取响应超时时间
	private Duration getResponseTimeout(Route route) {
		Object responseTimeoutAttr = route.getMetadata().get(RESPONSE_TIMEOUT_ATTR);
		Long responseTimeout = null;
		if (responseTimeoutAttr != null) {
			if (responseTimeoutAttr instanceof Number) {
				responseTimeout = ((Number) responseTimeoutAttr).longValue();
			}
			else {
				responseTimeout = Long.valueOf(responseTimeoutAttr.toString());
			}
		}
		return responseTimeout != null ? Duration.ofMillis(responseTimeout)
				: properties.getResponseTimeout();
	}

}

ForwardRoutingFilter源码分析

判断是不是forward,是则将请求转发给 DispatcherHandler,当作当前实例的url处理

public class ForwardRoutingFilter implements GlobalFilter, Ordered {

	private static final Log log = LogFactory.getLog(ForwardRoutingFilter.class);

	private final ObjectProvider<DispatcherHandler> dispatcherHandlerProvider;

	// do not use this dispatcherHandler directly, use getDispatcherHandler() instead.
	private volatile DispatcherHandler dispatcherHandler;

	public ForwardRoutingFilter(
			ObjectProvider<DispatcherHandler> dispatcherHandlerProvider) {
		this.dispatcherHandlerProvider = dispatcherHandlerProvider;
	}

	private DispatcherHandler getDispatcherHandler() {
		if (dispatcherHandler == null) {
			dispatcherHandler = dispatcherHandlerProvider.getIfAvailable();
		}

		return dispatcherHandler;
	}

    //Integer.MIN_VALUE
	@Override
	public int getOrder() {
		return Ordered.LOWEST_PRECEDENCE;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获得 request uri
		URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

        // 判断是否能够处理
        //加入请求url为http://127.0.0.1:8080/globalfilters,route的目标url为forward:///globalfilters
        //那么它会将请求转发给 DispatcherHandler,DispatcherHandler 匹配并转发到当前网关实例本地接口/globalfilters
		String scheme = requestUrl.getScheme();
		if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) {
			return chain.filter(exchange);
		}

		// TODO: translate url?

		if (log.isTraceEnabled()) {
			log.trace("Forwarding to URI: " + requestUrl);
		}

        //将请求转发给 DispatcherHandler,当作当前实例的链接处理
		return this.getDispatcherHandler().handle(exchange);
	}

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值