SpringBoot整合HttpClient结合线程池,且简单模仿MyBatisPlus设计思想

背景:

近期想写一个针对某高校系统的套壳系统,提高其安全与使用便利性,优化用户体验,使用的第三方库为HttpClient。且最近简单了解了MyBatisPlus在MyBatis 的基础上只做增强不做改变的设计,所以简单用HttpClient进行了设计,不再使用原始的设计为工具类。

代码:

HttpClient配置类

package com.xjl.common.config;

import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * HttpClient配置类
 *
 * @author lkb
 * @version 1.0
 */
@Configuration
@ConfigurationProperties(prefix = "http")
public class HttpClientConfig {

    @Value("${http.maxTotal:100}")
    private Integer maxTotal;

    @Value("${http.defaultMaxPerRoute:20}")
    private Integer defaultMaxPerRoute;

    @Value("${http.connectTimeout:5000}")
    private Integer connectTimeout;

    @Value("${http.connectionRequestTimeout:5000}")
    private Integer connectionRequestTimeout;

    @Value("${http.socketTimeout:5000}")
    private Integer socketTimeout;

    /**
     * 初始化连接池管理器,设置最大总连接数和并发连接数。
     *
     * @return 配置好的PoolingHttpClientConnectionManager。
     */
    @Bean(name = "httpClientConnectionManager")
    public PoolingHttpClientConnectionManager getHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
        // 最大总连接数
        httpClientConnectionManager.setMaxTotal(maxTotal);
        // 并发连接数
        httpClientConnectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
        return httpClientConnectionManager;
    }


    /**
     * 初始化连接池,设置连接池管理器。
     * 连接池管理器作为参数被注入。
     *
     * @param httpClientConnectionManager 先前配置的连接池管理器。
     * @return 配置好的HttpClientBuilder。
     */
    @Bean(name = "httpClientBuilder")
    public HttpClientBuilder getHttpClientBuilder(@Qualifier("httpClientConnectionManager") PoolingHttpClientConnectionManager httpClientConnectionManager) {
        // HttpClientBuilder的构造方法是protected的,因此我们使用静态的create()方法来获取实例。
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();

        httpClientBuilder.setConnectionManager(httpClientConnectionManager);

        return httpClientBuilder;
    }

    /**
     * 注入连接池,用于获取httpClient。
     *
     * @param httpClientBuilder 配置好的HttpClientBuilder。
     * @return 配置好的CloseableHttpClient。
     */
    @Bean
    public CloseableHttpClient getCloseableHttpClient(@Qualifier("httpClientBuilder") HttpClientBuilder httpClientBuilder) {
        return httpClientBuilder.build();
    }

    /**
     * Builder是RequestConfig的一个内部类。
     * 通过RequestConfig的custom方法获取Builder对象。
     * 设置builder的连接信息。
     * 这里还可以设置代理、cookieSpec等属性,有需要的话可以在此设置。
     *
     * @return 配置好的RequestConfig.Builder。
     */
    @Bean(name = "builder")
    public RequestConfig.Builder getBuilder() {
        RequestConfig.Builder builder = RequestConfig.custom();
        return builder.setConnectTimeout(connectTimeout)
                .setConnectionRequestTimeout(connectionRequestTimeout)
                .setSocketTimeout(socketTimeout)
                .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
//                .setProxy(HttpHost.create("127.0.0.1:8080"))
                ;
    }

    /**
     * 使用builder构建一个RequestConfig对象。
     *
     * @param builder 配置好的RequestConfig.Builder。
     * @return 配置好的RequestConfig。
     */
    @Bean
    public RequestConfig getRequestConfig(@Qualifier("builder") RequestConfig.Builder builder) {
        return builder.build();
    }

    public Integer getMaxTotal() {
        return maxTotal;
    }

    public void setMaxTotal(Integer maxTotal) {
        this.maxTotal = maxTotal;
    }

    public Integer getDefaultMaxPerRoute() {
        return defaultMaxPerRoute;
    }

    public void setDefaultMaxPerRoute(Integer defaultMaxPerRoute) {
        this.defaultMaxPerRoute = defaultMaxPerRoute;
    }

    public Integer getConnectTimeout() {
        return connectTimeout;
    }

    public void setConnectTimeout(Integer connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public Integer getConnectionRequestTimeout() {
        return connectionRequestTimeout;
    }

    public void setConnectionRequestTimeout(Integer connectionRequestTimeout) {
        this.connectionRequestTimeout = connectionRequestTimeout;
    }

    public Integer getSocketTimeout() {
        return socketTimeout;
    }

    public void setSocketTimeout(Integer socketTimeout) {
        this.socketTimeout = socketTimeout;
    }


}


HttpClient连接定时关闭线程

package com.xjl.common.config;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.pool.PoolStats;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * HttpClient 连接关闭线程
 * 在应用关闭时清理无效连接
 *
 * @author lkb
 * @version 1.0
 */
@Component
@Slf4j
public class HttpClientClose extends Thread {
    @Resource
    private PoolingHttpClientConnectionManager manager;
    /**
     * 开关 volatitle表示多线程可变数据,一个线程修改,其他线程立即修改
     */
    private volatile boolean shutdown;

    public HttpClientClose() {
        ///System.out.println("执行构造方法,实例化对象");
        //线程开启启动
        this.start();
    }


    @Override
    public void run() {
        try {
            // 如果服务没有关闭,执行线程
            while (!shutdown) {
                synchronized (this) {
                    // 等待5秒
                    wait(5000);
                    // 关闭超时的连接
                    PoolStats stats = manager.getTotalStats();
                    // 获取可用的线程数量
                    int av = stats.getAvailable();
                    // 获取阻塞的线程数量
                    int pend = stats.getPending();
                    // 获取当前正在使用的连接数量
                    int lea = stats.getLeased();
                    int max = stats.getMax();
                    manager.closeExpiredConnections();
                    manager.closeIdleConnections(30L, TimeUnit.SECONDS);
                    log.debug("Available: {}, Pending: {}, Leased: {}, Max: {}", av, pend, lea, max);
                }
            }
        } catch (InterruptedException e) {
            log.error("HttpClientClose thread interrupted.", e);
            // 保持中断状态
            Thread.currentThread().interrupt();
        }
    }

    /**
     * 关闭清理无效连接的线程
     */
    @PreDestroy
    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            // 全部从等待中唤醒.执行关闭操作;
            notifyAll();
        }
    }
}

使用MyBatisPlus设计思路对HttpClient进行功能增强

package com.xjl.common.service;

import org.apache.http.HttpResponse;
import org.apache.http.entity.mime.content.ContentBody;

import java.io.File;
import java.util.Map;

/**
 * @author lkb
 * @version 1.0
 */
public interface HttpClientService {


    /**
     * 发送GET请求并返回响应内容。
     *
     * @param url 请求的URL
     * @return 响应内容
     */
    HttpResponse httpGet(String url);

    /**
     * 发送带参数的GET请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求的参数
     * @return 响应内容
     */
    HttpResponse httpGet(String url, Map<String, String> headers);


    /**
     * 发送带参数和自定义请求头的GET请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求的自定义头部信息
     * @param params  请求的参数
     * @return 响应内容
     */
    HttpResponse httpGet(String url, Map<String, String> headers, Map<String, Object> params);


    /**
     * 发送POST请求并返回响应内容。
     *
     * @param url 请求的URL
     * @return 响应内容
     */
    HttpResponse httpPost(String url);


    /**
     * 发送带JSON数据的POST请求并返回响应内容。
     *
     * @param url  请求的URL
     * @param json JSON数据
     * @return 响应内容
     */
    HttpResponse httpPost(String url, String json);


    /**
     * 发送带参数的POST请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求头
     * @return 响应内容
     */
    HttpResponse httpPost(String url, Map<String, String> headers);


    /**
     * 发送带参数和自定义请求头的POST请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求的自定义头部信息
     * @param params  请求的参数
     * @return 响应内容
     */
    HttpResponse httpPost(String url, Map<String, String> headers, Map<String, Object> params);


    HttpResponse httpPost(String url, Map<String, String> headers, Map<String, Object> textParams, Map<String, File> fileParams, Map<String, Object> textNextParams);


    /**
     * 发送带参数和自定义请求头的POST请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求的自定义头部信息
     * @param params  请求的参数
     * @return 响应内容
     */
    HttpResponse httpPostFileMultiPart(String url, Map<String, String> headers, Map<String, ContentBody> params);


    /**
     * 解析URL中的参数,并将它们存储在一个Map中
     *
     * @param url 指定url
     * @return 返回map对象
     */
    Map<String, Object> extractUrlParameters(String url);

}
package com.xjl.common.service.impl;

import com.xjl.common.exception.EasyInvoiceException;
import com.xjl.common.service.HttpClientService;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 实现 {@link HttpClientService} 接口,用于进行 HTTP 请求。
 *
 * @author lkb
 * @version 1.0
 */
@Slf4j
public class HttpClientServiceImpl implements HttpClientService {

    private static final String BOUNDARY_PREFIX = "------WebKitFormBoundary";
    @Resource
    private CloseableHttpClient httpClient;
    @Resource
    private RequestConfig config;

    /**
     * 发送GET请求并返回响应内容。
     *
     * @param url 请求的URL
     * @return 响应内容
     */
    @Override
    public CloseableHttpResponse httpGet(String url) {
        return httpGet(url, null, new LinkedHashMap<>());
    }

    /**
     * 发送带参数的GET请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求的参数
     * @return 响应内容
     */
    @Override
    public CloseableHttpResponse httpGet(String url, Map<String, String> headers) {
        return httpGet(url, headers, null);
    }

    /**
     * 发送带参数和自定义请求头的GET请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求的自定义头部信息
     * @param params  请求的参数
     * @return 响应内容
     */
    @Override
    public CloseableHttpResponse httpGet(String url, Map<String, String> headers, Map<String, Object> params) {
        if (headers != null) {
            headers.putAll(preHeaders());
        } else {
            headers = preHeaders();
        }

        try {
            List<NameValuePair> pairs = convertParams(params);
            URIBuilder uriBuilder = new URIBuilder(url).addParameters(pairs);
            HttpGet httpGet = new HttpGet(uriBuilder.build());
            httpGet.setConfig(config);
            headers.forEach((key, value) -> httpGet.addHeader(key, String.valueOf(value)));
            return executeRequest(httpGet);
        } catch (Exception e) {
            throw new RuntimeException("执行GET请求失败", e);
        }
    }


    /**
     * 发送POST请求并返回响应内容。
     *
     * @param url 请求的URL
     * @return 响应内容
     */
    @Override
    public CloseableHttpResponse httpPost(String url) {
        return httpPost(url, null, null);
    }

    /**
     * 发送带JSON数据的POST请求并返回响应内容。
     *
     * @param url  请求的URL
     * @param json JSON数据
     * @return 响应内容
     */
    @Override
    public CloseableHttpResponse httpPost(String url, String json) {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(config);
        StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8);
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        return executeRequest(httpPost);
    }

    /**
     * 发送带参数的POST请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求头
     * @return 响应内容
     */
    @Override
    public CloseableHttpResponse httpPost(String url, Map<String, String> headers) {
        return httpPost(url, headers, null);
    }

    /**
     * 发送带参数和自定义请求头的POST请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求的自定义头部信息
     * @param params  请求的参数
     * @return 响应内容
     */
    @Override
    public CloseableHttpResponse httpPost(String url, Map<String, String> headers, Map<String, Object> params) {
        if (headers != null) {
            headers.putAll(preHeaders());
        } else {
            headers = preHeaders();
        }

        List<NameValuePair> pairs = convertParams(params);
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(config);
        httpPost.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8));
        headers.forEach((key, value) -> httpPost.addHeader(key, String.valueOf(value)));
        return executeRequest(httpPost);
    }

    /**
     * 发送带参数和自定义请求头的POST请求并返回响应内容。
     *
     * @param url        请求的URL
     * @param headers    请求的自定义头部信息
     * @param textParams 请求的参数
     * @param fileParams 上传的文件
     * @return 响应内容
     */
    @Override
    public CloseableHttpResponse httpPost(String url, Map<String, String> headers, Map<String, Object> textParams, Map<String, File> fileParams, Map<String, Object> textNextParams) {
        headers.putAll(preHeaders());
        String boundary = generateBoundary();
        headers.put("Content-Type", "multipart/form-data; boundary=" + boundary);
        MultipartEntityBuilder builder = MultipartEntityBuilder.create();

        if (textParams != null) {
            addTextParams(builder, textParams);
        }


        if (fileParams != null) {
            addFileParams(builder, fileParams);
        }
        if (textParams != null) {
            addTextParams(builder, textNextParams);
        }
        //自定义boundary
        HttpEntity httpEntity = builder.setBoundary(boundary).setContentType(ContentType.MULTIPART_FORM_DATA).setCharset(StandardCharsets.UTF_8).setMode(HttpMultipartMode.BROWSER_COMPATIBLE).build();
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(config);
        httpPost.setEntity(httpEntity);
        headers.forEach((key, value) -> httpPost.addHeader(key, String.valueOf(value)));
        return executeRequest(httpPost);
    }

    /**
     * 发送带参数和自定义请求头的POST请求并返回响应内容。
     *
     * @param url     请求的URL
     * @param headers 请求的自定义头部信息
     * @param params  请求的参数
     * @return 响应内容
     */
    @Override
    public CloseableHttpResponse httpPostFileMultiPart(String url, Map<String, String> headers, Map<String, ContentBody> params) {
        headers.putAll(preHeaders());
        MultipartEntityBuilder builder = MultipartEntityBuilder.create();

        for (Map.Entry<String, ContentBody> param : params.entrySet()) {
            builder.addPart(param.getKey(), param.getValue());
        }

        HttpEntity httpEntity = builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE).build();

        //自定义boundary
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(config);
        httpPost.setEntity(httpEntity);
        headers.forEach((key, value) -> httpPost.addHeader(key, String.valueOf(value)));
        return executeRequest(httpPost);
    }


    /**
     * 解析URL中的参数,并将它们存储在一个Map中
     *
     * @param url 指定url
     * @return 返回map对象
     */
    @Override
    public Map<String, Object> extractUrlParameters(String url) {
        Map<String, Object> keyMap = new LinkedHashMap<>();
        String[] parts = url.split("\\?");
        if (parts.length == 2) {
            String queryString = parts[1];
            Arrays.stream(queryString.split("&"))
                    .map(param -> param.split("="))
                    .filter(keyValue -> keyValue.length == 2)
                    .forEach(keyValue -> keyMap.put(keyValue[0], keyValue[1]));
        }
        return keyMap;
    }

    private Map<String, String> preHeaders() {
        Map<String, String> headers = new HashMap<>(12);
        headers.put(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.43");
        headers.put("Upgrade-Insecure-Requests", "1");
        headers.put("Connection", "keep-alive");
        headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
        headers.put("Accept-Encoding", "gzip, deflate, br");
        headers.put("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
        return headers;
    }

    private CloseableHttpResponse executeRequest(HttpRequestBase request) {

        try {
            return httpClient.execute(request);
        } catch (IOException e) {
            throw new EasyInvoiceException("执行HTTP请求失败", e);
        }
    }

    private List<NameValuePair> convertParams(Map<String, Object> params) {
        return Optional.ofNullable(params)
                .map(map -> map.entrySet().stream().map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue().toString())))
                .orElseGet(Stream::empty)
                .collect(Collectors.toList());
    }

    private void addTextParams(MultipartEntityBuilder builder, Map<String, Object> textParams) {
        Optional.ofNullable(textParams).ifPresent(params ->
                params.forEach((key, value) -> builder.addTextBody(key, (String) value, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8))));
    }

    private void addFileParams(MultipartEntityBuilder builder, Map<String, File> fileParams) {
        Optional.ofNullable(fileParams).ifPresent(params ->
                params.forEach((key, file) -> {
                    if (file != null) {
                        builder.addBinaryBody(key, file);
                    }
                })
        );
    }


    private String generateBoundary() {
        SecureRandom random = new SecureRandom();
        byte[] randomBytes = new byte[16];
        random.nextBytes(randomBytes);
        String base64Encoded = Base64.getEncoder().encodeToString(randomBytes);
        return BOUNDARY_PREFIX + base64Encoded.replace("/", "_").replace("+", "-").replace("=", "");
    }
}

应用实例

直接像使用MyBatisPlus的使用方式,继承其实现类调用接口即可

package com.xjl.api.service.impl;

import com.xjl.api.service.HttpLoginService;
import com.xjl.api.vo.UniversityUserVo;
import com.xjl.common.service.impl.HttpClientServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 实现 {@link HttpLoginService} 接口,用于用户登录获取信息
 *
 * @author lkb
 * @version 1.0
 */
@Service
public class HttpLoginServiceImpl extends HttpClientServiceImpl implements HttpLoginService {

    private static final String COOKIE_PATTERN = "(\\S+?)=(\\S+?);";

    private static final String BASE_URL = "XXXXXXXXXX";

    /**
     * 从JavaScript代码中提取alert中的提示信息
     *
     * @param scriptText alert中的文本
     * @return 提取alert中的文本
     */
    private static String extractAlertMessage(String scriptText) {
        String alertPrefix = "alert('";
        String alertSuffix = "')";

        int startIndex = scriptText.indexOf(alertPrefix);
        int endIndex = scriptText.indexOf(alertSuffix, startIndex + alertPrefix.length());

        if (startIndex != -1 && endIndex != -1) {
            return scriptText.substring(startIndex + alertPrefix.length(), endIndex);
        } else {
            // 没找到alert提示信息
            return null;
        }
    }

    private void getWebApp(UniversityUserVo userVo) throws IOException {
        CloseableHttpResponse response = httpGet(BASE_URL + "/wsyy");
        HttpEntity entity = response.getEntity();
        try {
            String html = EntityUtils.toString(entity, "utf-8");
            Document document = Jsoup.parse(html);
            Element viewStateInput = document.body().select("input[id=__VIEWSTATE]").first();
            Element eventValidationInput = document.body().select("input[id=__EVENTVALIDATION]").first();
            Element viewStateGeneratorInput = document.body().select("input[id=__VIEWSTATEGENERATOR]").first();
            if (viewStateInput == null || eventValidationInput == null || viewStateGeneratorInput == null) {
                throw new RuntimeException("获取系统信息异常");
            }
            String viewStateValue = viewStateInput.attr("value");
            String eventValidationValue = eventValidationInput.attr("value");
            String viewStateGeneratorValue = viewStateGeneratorInput.attr("value");
            Map<String, Object> dataKeys = new HashMap<>(6);

            dataKeys.put("__VIEWSTATE", viewStateValue);
            dataKeys.put("__EVENTVALIDATION", eventValidationValue);
            dataKeys.put("__VIEWSTATEGENERATOR", viewStateGeneratorValue);
            userVo.setDataKeys(dataKeys);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            response.close();
        }
    }



}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神风静默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值