多线程下载 断点续传

本文介绍了一个基于Android的下载管理器实现方案,包括依赖库、权限配置、UI布局及核心类设计等内容。该下载管理器支持断点续传、多线程下载、进度更新等功能。
    //依赖  
        compile 'com.squareup.okhttp3:okhttp:3.6.0'  
        compile 'com.squareup.okio:okio:1.11.0'  
        compile 'fm.jiecao:jiecaovideoplayer:5.5'
      
    //权限  
        <!-- 联网权限 -->  
        <uses-permission android:name="android.permission.INTERNET" />  
        <!-- 读取手机状态权限 -->  
        <uses-permission android:name="android.permission.READ_PHONE_STATE" />  
        <!-- 往sdcard中写入数据的权限 -->  
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  
        <!-- 读取sdcard中数据的权限 -->  
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />  
        <!-- 在sdcard中创建/删除文件的权限 -->  
        <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />  
      
      
    //main.xml  
    <?xml version="1.0" encoding="utf-8"?>  
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        xmlns:tools="http://schemas.android.com/tools"  
        android:id="@+id/activity_main"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        android:orientation="vertical">  
      
      
      
        <LinearLayout  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:layout_marginTop="15dp"  
            android:gravity="center_vertical"  
            android:orientation="horizontal">  
      
      
            <TextView  
                android:id="@+id/tv_progress2"  
                android:layout_width="wrap_content"  
                android:layout_height="wrap_content"  
                android:layout_marginLeft="15dp"  
                android:text="0%" />  
      
      
        </LinearLayout>  
      
      
        <ProgressBar  
            android:id="@+id/pb_progress2"  
            style="?android:attr/progressBarStyleHorizontal"  
            android:layout_width="match_parent"  
            android:layout_height="40dp"  
            android:layout_margin="20dp" />  
      
        <LinearLayout  
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:layout_gravity="center_horizontal"  
            android:orientation="horizontal">  
      
            <Button  
                android:id="@+id/btn_download2"  
                android:layout_width="wrap_content"  
                android:layout_height="wrap_content"  
                android:onClick="downloadOrPause"  
                android:text="下载" />  
      
            <Button  
                android:id="@+id/btn_cancel2"  
                android:layout_width="wrap_content"  
                android:layout_height="wrap_content"  
                android:layout_marginLeft="10dp"  
                android:onClick="cancel"  
                android:text="取消" />  
      
        </LinearLayout>  
      


   <fm.jiecao.jcvideoplayer_lib.JCVideoPlayerStandard
        android:id="@+id/video_view"
        android:layout_width="match_parent"
        android:layout_height="250dp"/>
      
    </LinearLayout>  
      
      
      
    //DownloadListner  
    public interface DownloadListner {  
        void onFinished();  
      
        void onProgress(float progress);  
      
        void onPause();  
      
        void onCancel();  
    }  
      
      
    //DownloadManager  
      
    import android.os.Environment;  
    import android.text.TextUtils;  
      
    import java.io.File;  
    import java.util.HashMap;  
    import java.util.Map;  
      
    /** 
     * 下载管理器,断点续传 
     */  
    public class DownloadManager {  
      
        private String DEFAULT_FILE_DIR;//默认下载目录  
        private Map<String, DownloadTask> mDownloadTasks;//文件下载任务索引,String为url,用来唯一区别并操作下载的文件  
        private static DownloadManager mInstance;  
        private static final String TAG = "DownloadManager";  
        /** 
         * 下载文件 
         */  
        public void download(String... urls) {  
            //单任务开启下载或多任务开启下载  
            for (int i = 0, length = urls.length; i < length; i++) {  
                String url = urls[i];  
                if (mDownloadTasks.containsKey(url)) {  
                    mDownloadTasks.get(url).start();  
                }  
            }  
        }  
      
      
        // 获取下载文件的名称  
        public String getFileName(String url) {  
            return url.substring(url.lastIndexOf("/") + 1);  
        }  
      
        /** 
         * 暂停 
         */  
        public void pause(String... urls) {  
            //单任务暂停或多任务暂停下载  
            for (int i = 0, length = urls.length; i < length; i++) {  
                String url = urls[i];  
                if (mDownloadTasks.containsKey(url)) {  
                    mDownloadTasks.get(url).pause();  
                }  
            }  
        }  
      
        /** 
         * 取消下载 
         */  
        public void cancel(String... urls) {  
            //单任务取消或多任务取消下载  
            for (int i = 0, length = urls.length; i < length; i++) {  
                String url = urls[i];  
                if (mDownloadTasks.containsKey(url)) {  
                    mDownloadTasks.get(url).cancel();  
                }  
            }  
        }  
      
        /** 
         * 添加下载任务 
         */  
        public void add(String url, DownloadListner l) {  
            add(url, null, null, l);  
        }  
      
        /** 
         * 添加下载任务 
         */  
        public void add(String url, String filePath, DownloadListner l) {  
            add(url, filePath, null, l);  
        }  
      
        /** 
         * 添加下载任务 
         */  
        public void add(String url, String filePath, String fileName, DownloadListner l) {  
            if (TextUtils.isEmpty(filePath)) {//没有指定下载目录,使用默认目录  
                filePath = getDefaultDirectory();  
            }  
            if (TextUtils.isEmpty(fileName)) {  
                fileName = getFileName(url);  
            }  
            mDownloadTasks.put(url, new DownloadTask(new FilePoint(url, filePath, fileName), l));  
        }  
      
        /** 
         * 默认下载目录 
         * @return 
         */  
        private String getDefaultDirectory() {  
            if (TextUtils.isEmpty(DEFAULT_FILE_DIR)) {  
                DEFAULT_FILE_DIR = Environment.getExternalStorageDirectory().getAbsolutePath()  
                        + File.separator + "icheny" + File.separator;  
            }  
            return DEFAULT_FILE_DIR;  
        }  
      
        public static DownloadManager getInstance() {//管理器初始化  
            if (mInstance == null) {  
                synchronized (DownloadManager.class) {  
                    if (mInstance == null) {  
                        mInstance = new DownloadManager();  
                    }  
                }  
            }  
            return mInstance;  
        }  
      
        public DownloadManager() {  
            mDownloadTasks = new HashMap<>();  
        }  
      
        /** 
         * 取消下载 
         */  
        public boolean isDownloading(String... urls) {  
            //这里传一个url就是判断一个下载任务  
            //多个url数组适合下载管理器判断是否作操作全部下载或全部取消下载  
            boolean result = false;  
            for (int i = 0, length = urls.length; i < length; i++) {  
                String url = urls[i];  
                if (mDownloadTasks.containsKey(url)) {  
                    result = mDownloadTasks.get(url).isDownloading();  
                }  
            }  
            return result;  
        }  
    }  
      
      
    //DownloadTask  
    import android.os.Handler;  
    import android.os.Message;  
    import android.util.Log;  
      
    import java.io.Closeable;  
    import java.io.File;  
    import java.io.IOException;  
    import java.io.InputStream;  
    import java.io.RandomAccessFile;  
      
    import okhttp3.Call;  
    import okhttp3.Response;  
      
      
      
    public class DownloadTask extends Handler {  
      
        private final int THREAD_COUNT = 4;//线程数  
        private FilePoint mPoint;  
        private long mFileLength;  
      
      
        private boolean isDownloading = false;  
        private int childCanleCount;//子线程取消数量  
        private int childPauseCount;//子线程暂停数量  
        private int childFinshCount;  
        private HttpUtil mHttpUtil;  
        private long[] mProgress;  
        private File[] mCacheFiles;  
        private File mTmpFile;//临时占位文件  
        private boolean pause;//是否暂停  
        private boolean cancel;//是否取消下载  
      
        private final int MSG_PROGRESS = 1;//进度  
        private final int MSG_FINISH = 2;//完成下载  
        private final int MSG_PAUSE = 3;//暂停  
        private final int MSG_CANCEL = 4;//暂停  
        private DownloadListner mListner;//下载回调监听  
      
        /** 
         * 任务管理器初始化数据 
         * @param point 
         * @param l 
         */  
        DownloadTask(FilePoint point, DownloadListner l) {  
            this.mPoint = point;  
            this.mListner = l;  
            this.mProgress = new long[THREAD_COUNT];  
            this.mCacheFiles = new File[THREAD_COUNT];  
            this.mHttpUtil = HttpUtil.getInstance();  
        }  
      
        /** 
         * 任务回调消息 
         * @param msg 
         */  
        @Override  
        public void handleMessage(Message msg) {  
            super.handleMessage(msg);  
            if (null == mListner) {  
                return;  
            }  
            switch (msg.what) {  
                case MSG_PROGRESS://进度  
                    long progress = 0;  
                    for (int i = 0, length = mProgress.length; i < length; i++) {  
                        progress += mProgress[i];  
                    }  
                    mListner.onProgress(progress * 1.0f / mFileLength);  
                    break;  
                case MSG_PAUSE://暂停  
                    childPauseCount++;  
                    if (childPauseCount % THREAD_COUNT != 0) return;  
                    resetStutus();  
                    mListner.onPause();  
                    break;  
                case MSG_FINISH://完成  
                    childFinshCount++;  
                    if (childFinshCount % THREAD_COUNT != 0) return;  
                    mTmpFile.renameTo(new File(mPoint.getFilePath(), mPoint.getFileName()));//下载完毕后,重命名目标文件名  
                    resetStutus();  
                    mListner.onFinished();  
                    break;  
                case MSG_CANCEL://取消  
                    childCanleCount++;  
                    if (childCanleCount % THREAD_COUNT != 0) return;  
                    resetStutus();  
                    mProgress = new long[THREAD_COUNT];  
                    mListner.onCancel();  
                    break;  
            }  
        }  
      
        private static final String TAG = "DownloadTask";  
      
        public synchronized void start() {  
            try {  
                Log.e(TAG, "start: " + isDownloading + "\t" + mPoint.getUrl());  
                if (isDownloading) return;  
                isDownloading = true;  
                mHttpUtil.getContentLength(mPoint.getUrl(), new okhttp3.Callback() {  
                    @Override  
                    public void onResponse(Call call, Response response) throws IOException {  
                        if (response.code() != 200) {  
                            close(response.body());  
                            resetStutus();  
                            return;  
                        }  
                        // 获取资源大小  
                        mFileLength = response.body().contentLength();  
                        close(response.body());  
                        // 在本地创建一个与资源同样大小的文件来占位  
                        mTmpFile = new File(mPoint.getFilePath(), mPoint.getFileName() + ".tmp");  
                        if (!mTmpFile.getParentFile().exists()) mTmpFile.getParentFile().mkdirs();  
                        RandomAccessFile tmpAccessFile = new RandomAccessFile(mTmpFile, "rw");  
                        tmpAccessFile.setLength(mFileLength);  
                        /*将下载任务分配给每个线程*/  
                        long blockSize = mFileLength / THREAD_COUNT;// 计算每个线程理论上下载的数量.  
      
                        /*为每个线程配置并分配任务*/  
                        for (int threadId = 0; threadId < THREAD_COUNT; threadId++) {  
                            long startIndex = threadId * blockSize; // 线程开始下载的位置  
                            long endIndex = (threadId + 1) * blockSize - 1; // 线程结束下载的位置  
                            if (threadId == (THREAD_COUNT - 1)) { // 如果是最后一个线程,将剩下的文件全部交给这个线程完成  
                                endIndex = mFileLength - 1;  
                            }  
                            download(startIndex, endIndex, threadId);// 开启线程下载  
                        }  
                    }  
      
                    @Override  
                    public void onFailure(Call call, IOException e) {  
                    }  
                });  
            } catch (IOException e) {  
                e.printStackTrace();  
                resetStutus();  
            }  
        }  
      
        public void download(final long startIndex, final long endIndex, final int threadId) throws IOException {  
            long newStartIndex = startIndex;  
            // 分段请求网络连接,分段将文件保存到本地.  
            // 加载下载位置缓存文件  
            final File cacheFile = new File(mPoint.getFilePath(), "thread" + threadId + "_" + mPoint.getFileName() + ".cache");  
            mCacheFiles[threadId] = cacheFile;  
            final RandomAccessFile cacheAccessFile = new RandomAccessFile(cacheFile, "rwd");  
            if (cacheFile.exists()) {// 如果文件存在  
                String startIndexStr = cacheAccessFile.readLine();  
                try {  
                    newStartIndex = Integer.parseInt(startIndexStr);//重新设置下载起点  
                } catch (NumberFormatException e) {  
                    e.printStackTrace();  
                }  
            }  
            final long finalStartIndex = newStartIndex;  
            mHttpUtil.downloadFileByRange(mPoint.getUrl(), finalStartIndex, endIndex, new okhttp3.Callback() {  
                @Override  
                public void onResponse(Call call, Response response) throws IOException {  
                    if (response.code() != 206) {// 206:请求部分资源成功码  
                        resetStutus();  
                        return;  
                    }  
                    InputStream is = response.body().byteStream();// 获取流  
                    RandomAccessFile tmpAccessFile = new RandomAccessFile(mTmpFile, "rw");// 获取前面已创建的文件.  
                    tmpAccessFile.seek(finalStartIndex);// 文件写入的开始位置.  
                      /*  将网络流中的文件写入本地*/  
                    byte[] buffer = new byte[1024 << 2];  
                    int length = -1;  
                    int total = 0;// 记录本次下载文件的大小  
                    long progress = 0;  
                    while ((length = is.read(buffer)) > 0) {  
                        if (cancel) {  
                            //关闭资源  
                            close(cacheAccessFile, is, response.body());  
                            cleanFile(cacheFile);  
                            sendEmptyMessage(MSG_CANCEL);  
                            return;  
                        }  
                        if (pause) {  
                            //关闭资源  
                            close(cacheAccessFile, is, response.body());  
                            //发送暂停消息  
                            sendEmptyMessage(MSG_PAUSE);  
                            return;  
                        }  
                        tmpAccessFile.write(buffer, 0, length);  
                        total += length;  
                        progress = finalStartIndex + total;  
      
                        //将当前现在到的位置保存到文件中  
                        cacheAccessFile.seek(0);  
                        cacheAccessFile.write((progress + "").getBytes("UTF-8"));  
                        //发送进度消息  
                        mProgress[threadId] = progress - startIndex;  
                        sendEmptyMessage(MSG_PROGRESS);  
                    }  
                    //关闭资源  
                    close(cacheAccessFile, is, response.body());  
                    // 删除临时文件  
                    cleanFile(cacheFile);  
                    //发送完成消息  
                    sendEmptyMessage(MSG_FINISH);  
                }  
      
                @Override  
                public void onFailure(Call call, IOException e) {  
                    isDownloading = false;  
                }  
            });  
        }  
      
        /** 
         * 关闭资源 
         * 
         * @param closeables 
         */  
        private void close(Closeable... closeables) {  
            int length = closeables.length;  
            try {  
                for (int i = 0; i < length; i++) {  
                    Closeable closeable = closeables[i];  
                    if (null != closeable)  
                        closeables[i].close();  
                }  
            } catch (IOException e) {  
                e.printStackTrace();  
            } finally {  
                for (int i = 0; i < length; i++) {  
                    closeables[i] = null;  
                }  
            }  
        }  
      
        /** 
         * 删除临时文件 
         */  
        private void cleanFile(File... files) {  
            for (int i = 0, length = files.length; i < length; i++) {  
                if (null != files[i])  
                    files[i].delete();  
            }  
        }  
      
        /** 
         * 暂停 
         */  
        public void pause() {  
            pause = true;  
        }  
      
        /** 
         * 取消 
         */  
        public void cancel() {  
            cancel = true;  
            cleanFile(mTmpFile);  
            if (!isDownloading) {  
                if (null != mListner) {  
                    cleanFile(mCacheFiles);  
                    resetStutus();  
                    mListner.onCancel();  
                }  
            }  
        }  
      
        /** 
         * 重置下载状态 
         */  
        private void resetStutus() {  
            pause = false;  
            cancel = false;  
            isDownloading = false;  
        }  
      
        public boolean isDownloading() {  
            return isDownloading;  
        }  
    }  
      
      
    //FilePoint  
    public class FilePoint {  
        private String fileName;//文件名  
        private String url;//下载地址  
        private String filePath;//下载目录  
      
        public FilePoint(String url) {  
            this.url = url;  
        }  
      
        public FilePoint(String filePath, String url) {  
            this.filePath = filePath;  
            this.url = url;  
        }  
      
        public FilePoint(String url, String filePath, String fileName) {  
            this.url = url;  
            this.filePath = filePath;  
            this.fileName = fileName;  
        }  
      
        public String getFileName() {  
            return fileName;  
        }  
      
        public void setFileName(String fileName) {  
            this.fileName = fileName;  
        }  
      
        public String getUrl() {  
            return url;  
        }  
      
        public void setUrl(String url) {  
            this.url = url;  
        }  
      
        public String getFilePath() {  
            return filePath;  
        }  
      
        public void setFilePath(String filePath) {  
            this.filePath = filePath;  
        }  
      
    }  
      
      
    //HttpUtil  
    public class HttpUtil {  
        private OkHttpClient mOkHttpClient;  
        private static HttpUtil mInstance;  
        private final static long CONNECT_TIMEOUT = 60;//超时时间,秒  
        private final static long READ_TIMEOUT = 60;//读取时间,秒  
        private final static long WRITE_TIMEOUT = 60;//写入时间,秒  
      
        /** 
         * @param url        下载链接 
         * @param startIndex 下载起始位置 
         * @param endIndex   结束为止 
         * @param callback   回调 
         * @throws IOException 
         */  
        public void downloadFileByRange(String url, long startIndex, long endIndex, Callback callback) throws IOException {  
            // 创建一个Request  
            // 设置分段下载的头信息。 Range:做分段数据请求,断点续传指示下载的区间。格式: Range bytes=0-1024或者bytes:0-1024  
            Request request = new Request.Builder().header("RANGE", "bytes=" + startIndex + "-" + endIndex)  
                    .url(url)  
                    .build();  
            doAsync(request, callback);  
        }  
      
        public void getContentLength(String url, Callback callback) throws IOException {  
            // 创建一个Request  
            Request request = new Request.Builder()  
                    .url(url)  
                    .build();  
            doAsync(request, callback);  
        }  
      
        /** 
         * 异步请求 
         */  
        private void doAsync(Request request, Callback callback) throws IOException {  
            //创建请求会话  
            Call call = mOkHttpClient.newCall(request);  
            //同步执行会话请求  
            call.enqueue(callback);  
        }  
      
        /** 
         * 同步请求 
         */  
        private Response doSync(Request request) throws IOException {  
      
            //创建请求会话  
            Call call = mOkHttpClient.newCall(request);  
            //同步执行会话请求  
            return call.execute();  
        }  
      
      
        /** 
         * @return HttpUtil实例对象 
         */  
        public static HttpUtil getInstance() {  
            if (null == mInstance) {  
                synchronized (HttpUtil.class) {  
                    if (null == mInstance) {  
                        mInstance = new HttpUtil();  
                    }  
                }  
            }  
            return mInstance;  
        }  
      
        /** 
         * 构造方法,配置OkHttpClient 
         */  
        public HttpUtil() {  
            //创建okHttpClient对象  
            OkHttpClient.Builder builder = new OkHttpClient.Builder()  
                    .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)  
                    .writeTimeout(READ_TIMEOUT, TimeUnit.SECONDS)  
                    .readTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS);  
            mOkHttpClient = builder.build();  
        }  
    }  
      
    //mainactivity  
      
    import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.VideoView;

import fm.jiecao.jcvideoplayer_lib.JCVideoPlayer;
import fm.jiecao.jcvideoplayer_lib.JCVideoPlayerStandard;


public class MainActivity extends AppCompatActivity {

    private static final int PERMISSION_REQUEST_CODE = 001;
    Button btn_download2;
    TextView tv_progress2;
    ProgressBar  pb_progress2;

    DownloadManager mDownloadManager;
    String Url = "http://pic.ibaotu.com/00/34/48/06n888piCANy.mp4";

    private VideoView view;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
        initDownloads();

    }


    @Override
    public void onBackPressed() {
        if (JCVideoPlayer.backPress()) {
            return;
        }
        super.onBackPressed();
    }
    @Override
    protected void onPause() {
        super.onPause();
        JCVideoPlayer.releaseAllVideos();
    }


    private void initDownloads() {
        mDownloadManager = DownloadManager.getInstance();


        mDownloadManager.add(Url, new DownloadListner() {
            @Override
            public void onFinished() {
                Toast.makeText(MainActivity.this, "下载完成!", Toast.LENGTH_SHORT).show();

                JCVideoPlayerStandard jc= (JCVideoPlayerStandard) findViewById(R.id.video_view);
                jc.setUp("http://pic.ibaotu.com/00/34/48/06n888piCANy.mp4",
                        JCVideoPlayerStandard.SCREEN_LAYOUT_NORMAL);
            }

            @Override
            public void onProgress(float progress) {
                pb_progress2.setProgress((int) (progress * 100));
                tv_progress2.setText(String.format("%.2f", progress * 100) + "%");
            }

            @Override
            public void onPause() {
                Toast.makeText(MainActivity.this, "暂停了!", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onCancel() {
                tv_progress2.setText("0%");
                pb_progress2.setProgress(0);
                btn_download2.setText("下载");
                Toast.makeText(MainActivity.this, "下载已取消!", Toast.LENGTH_SHORT).show();
            }
        });
    }

    /**
     * 初始化View控件
     */
    private void initViews() {


        tv_progress2 = (TextView) findViewById(R.id.tv_progress2);
        pb_progress2 = (ProgressBar) findViewById(R.id.pb_progress2);
        btn_download2 = (Button) findViewById(R.id.btn_download2);


    }

    /**
     * 下载或暂停下载
     */
    public void downloadOrPause(View view) {

        if (!mDownloadManager.isDownloading(Url)) {
            mDownloadManager.download(Url);
            btn_download2.setText("暂停");
        } else {
            btn_download2.setText("下载");
            mDownloadManager.pause(Url);
        }

    }


    /**
     * 取消下载
     */
    public void cancel(View view) {

        switch (view.getId()) {
            case R.id.btn_cancel2:
                mDownloadManager.cancel(Url);
                break;
        }
    }

    public void cancelAll(View view) {
        btn_download2.setText("下载");
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return;
        }
        String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;

        if (!checkPermission(permission)) {//针对android6.0动态检测申请权限
            if (shouldShowRationale(permission)) {
                showMessage("需要权限跑demo哦...");
            }
            ActivityCompat.requestPermissions(this, new String[]{permission}, PERMISSION_REQUEST_CODE);
        }
    }

    /**
     * 显示提示消息
     */
    private void showMessage(String msg) {
        Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
    }

    /**
     * 检测用户权限
     */
    protected boolean checkPermission(String permission) {
        return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * 是否需要显示请求权限的理由
     */
    protected boolean shouldShowRationale(String permission) {
        return ActivityCompat.shouldShowRequestPermissionRationale(this, permission);
    }

}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值