unityEditor利用自定义窗口打包AssetBundle

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
//初始版本
//参考https://blog.csdn.net/damenhanter/article/details/50221841
public class ExportAssetBundles : EditorWindow
{
    
    [MenuItem("Build/ExportResource")]
    static void ExportResource()
    {
        //打包方式一
        //打开保存文件面板,获取用户选择的路径  
        //string path = EditorUtility.SaveFilePanel("Save Resource","" ,"New Resource", "assetbundle");
        //Debug.Log(path);//保持AB包路径
        //if(path.Length!= 0)
        //{
        //    //选择要保存的对象      选择模式包含文件夹
        //    Object[] selection = Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets);

        //    //打包
        //    BuildPipeline.BuildAssetBundle(Selection.activeObject,selection,path,BuildAssetBundleOptions.CollectDependencies
        //        |BuildAssetBundleOptions.CompleteAssets,BuildTarget.StandaloneWindows);
        //    //BuildPipeline.BuildAssetBundles(path, BuildAssetBundleOptions.CollectDependencies
        //    //    | BuildAssetBundleOptions.CompleteAssets, BuildTarget.StandaloneWindows);
        //}

        //打包方式二   现在常用方式
        //string outPath = Application.dataPath + "/Test";
        //string resPath = Application.dataPath + "/Prefabs";
        //SetAssetBundleName(new DirectoryInfo(resPath));//设置每个资源的ab包名
        //Caching.ClearCache();//清除内存ab包缓存
        //BuildPipeline.BuildAssetBundles(outPath, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
        //AssetDatabase.Refresh();//刷新unity文件夹


        //尝试手动选择  资源路径和ab输出路径
        //string path = EditorUtility.OpenFolderPanel("", "", "");
        //Debug.Log(path);//得到选中的文件夹

    }

  //设置ab包名  根据资源所在文件夹
    private static void SetAssetBundleName(DirectoryInfo directoryInfo)
    {
        DirectoryInfo[] directoryInfos = directoryInfo.GetDirectories();//获取当前目录的子目录(不包含孙子目录)
        Debug.Log(directoryInfos.Length);
        FileInfo[] fileInfos = directoryInfo.GetFiles();//获取目录中的文件,不包含子目录当中的文件  包含.meta文件
        Debug.Log(fileInfos.Length);
        for (int i = 0; i < directoryInfos.Length; i++)
        {
            SetAssetBundleName(directoryInfos[i]);
        }
        for (int i = 0; i < fileInfos.Length; i++)
        {
            FileInfo info = fileInfos[i];
            //根据扩展名来判断
            if (info.Extension != ".meta" && info.Extension != ".cs")
            {
                string path = info.FullName.Replace(@"\", @"/").Replace(Application.dataPath.Replace(@"\", @"/"), "");
                path = "Assets" + path;
                Debug.Log(path);
                //TODO  
                string assetbundleName = path.Replace("Assets" + "/Prefabs" + "/", "").Replace(info.Extension, "");
                AssetImporter assetImporter = AssetImporter.GetAtPath(path);
                assetImporter.assetBundleName = assetbundleName;
            }
        }
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System;
using System.Security.Cryptography;

/// <summary>
/// 通过自定义窗口打包ab包,并且压缩成zip  把zip放在StreamingAssets文件夹下,安卓第一次运行时拷贝到Application.persistentDataPath解压
/// 生成版本号
/// 存储abname和MD5值以及文件大小在一个文件中  用于比对
/// </summary>
public class ExportAssetBundles : EditorWindow
{
    //脚本无需打包成ab包  .lua .cs  .json

    [MenuItem("Build/ExportResource")]
    static void ExportResource()
    {
        //使用UnityEditorWindow自建窗口扩展  提示
        ExportAssetBundles myWindow = (ExportAssetBundles)EditorWindow.GetWindow<ExportAssetBundles>();
        myWindow.minSize = new Vector2(800, 600);
        myWindow.Show();
    }
   

    static string resPath = "";//需要打包的资源路径  统一放在一个文件夹下
    static string outPath = "";//打包的ab包输出路径  统一放在一个文件夹下
    static string zipPath = "";//打包的ab包压缩包输出路径  
    static string version = "";//版本号
    static  MyZip myZip;
    BuildTarget buildtarget = BuildTarget.StandaloneWindows;//设置打包平台
    private void OnGUI()
    {
        GUILayout.BeginVertical();//开始竖直方向排列    
        GUILayout.Space(5);//竖向间隔

        //设置需要打包的资源的路径
        GUILayout.BeginHorizontal();
        GUI.skin.label.fontSize = 14;
        GUILayout.Label("Build Resources Path:");
        GUILayout.Space(5);//横向间隔
        resPath = EditorGUILayout.TextField(resPath);
        GUILayout.Space(5);//横向间隔
        if (GUILayout.Button("请选择需要打包的资源路径文件夹"))
        {
            resPath = EditorUtility.OpenFolderPanel("", "", "");//得到选中的文件夹
        }   
        GUILayout.EndHorizontal();
        GUILayout.Space(5);//间隔

        //设置已经打包的ab包的输出路径
        GUILayout.BeginHorizontal();
        GUI.skin.label.fontSize = 14;
        GUILayout.Label("AssetBundle Output Path:");
        GUILayout.Space(5);//横向间隔
        outPath = EditorGUILayout.TextField(outPath);
        GUILayout.Space(5);//横向间隔
        if (GUILayout.Button("请选择打包输出路径"))
        {
            outPath = EditorUtility.OpenFolderPanel("", "", "");//得到选中的文件夹
        }
        GUILayout.EndHorizontal();

        //设置压缩ab包zip的输出路径
        GUILayout.BeginHorizontal();
        GUI.skin.label.fontSize = 14;
        GUILayout.Label("AssetBundle Zip Output Path:");
        GUILayout.Space(5);//横向间隔
        zipPath = EditorGUILayout.TextField(zipPath);
        GUILayout.Space(5);//横向间隔
        if (GUILayout.Button("请选择压缩ab包的输出路径"))
        {
            zipPath = EditorUtility.SaveFilePanel("assets", "","assetbundles", "zip");//得到选中的文件夹
        }
        GUILayout.EndHorizontal();


        //选择打包的平台
        GUILayout.Space(5);//竖向间隔
        GUILayout.BeginHorizontal();
        GUILayout.Label("Select Platform:");
        buildtarget = (BuildTarget)EditorGUILayout.EnumPopup(buildtarget);
        GUILayout.EndHorizontal();


        //打包的版本  assetbundle  version
        //todo
        GUILayout.Space(5);//竖向间隔
        GUILayout.BeginHorizontal();
        GUI.skin.label.fontSize = 14;
        GUILayout.Label("AssetBundle Version:");
        GUILayout.Space(5);//横向间隔
        version = EditorGUILayout.TextField(version);
        GUILayout.Space(5);//横向间隔
        if (GUILayout.Button("生成版本文件"))
        {
            if (version == "")
            {
                //弹出unity提示窗口 
                EditorUtility.DisplayDialog("提示", "生成失败,请确认已经填写版本号", "理解");
            }
            else
            {
                if (resPath == "")
                {
                    EditorUtility.DisplayDialog("提示", "生成失败,请确认已经填写打包资源路径", "理解");
                }
                else
                {
                    if (!Directory.Exists(Application.streamingAssetsPath))
                    {
                        Directory.CreateDirectory(Application.streamingAssetsPath);
                    }
                    File.WriteAllText(Path.Combine(Application.streamingAssetsPath, "version.txt"), version);
                    File.WriteAllText(Path.Combine(resPath, "version.txt"), version);
                }
              
            }
            
        }
        GUILayout.EndHorizontal();
        

        //开始打包
        GUILayout.Space(5);//竖向间隔
        if (GUILayout.Button("开始打包AssetBundle"))
        {
            if(resPath != "" && outPath != ""&&zipPath!="")
            {
                //开始打包
                //此处可以根据文件类型进行分类  暂不分类
                SetAssetBundleName(new DirectoryInfo(resPath));//设置每个资源的ab包名
                Caching.ClearCache();//清除内存ab包缓存
                BuildPipeline.BuildAssetBundles(outPath, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
                AssetDatabase.Refresh();//刷新unity文件夹
                Caching.ClearCache();//清除内存ab包缓存
                Debug.Log("打包完成");
                Debug.Log("需要打包的资源所在目录:" + resPath + "   打包后的AssetBundle所在目录:" + outPath);

            }
            else
            {
                //弹出unity提示窗口 
                EditorUtility.DisplayDialog("提示","打包失败,请确认已经选择路径","理解");
            } 
        }

        //打包完生成MD5文件
        GUILayout.Space(5);//竖向间隔
        if (GUILayout.Button("生成md5的file文件"))
        {
            if ( outPath != "" )
            {
                //获取所有ab文件md5   写进文件file.json
                Caching.ClearCache();//清除内存ab包缓存
                string totalABPath = outPath.Substring(outPath.LastIndexOf("/") + 1);
                AssetBundle ab = AssetBundle.LoadFromFile(outPath+"/"+totalABPath);//加载所有AssetBundle
                AssetBundleManifest manifest = (AssetBundleManifest)ab.LoadAsset("AssetBundleManifest");//加载对应AssetBundleManifest
        
                ab.Unload(false);
                string[] abnames = manifest.GetAllAssetBundles();//获取所有ab包名

                for (int i = 0; i < abnames.Length; i++)
                {
                    Debug.Log(abnames[i]);
                }
                List<AssetDesc> abInfo = new List<AssetDesc>();//存储MD5
                abInfo.Add(new AssetDesc(totalABPath, GetFileMd5(outPath + "/" + totalABPath), File.ReadAllBytes(outPath + "/" + totalABPath).Length));
                abInfo.Add(new AssetDesc(totalABPath + ".manifest", GetFileMd5(outPath + "/" + totalABPath + ".manifest"), File.ReadAllBytes(outPath + "/" + totalABPath + ".manifest").Length));

                foreach (string name in abnames)
                {
                    //没有后缀的文件
                    if (File.Exists(Path.Combine(outPath, name)))
                    {
                        abInfo.Add(new AssetDesc(name, GetFileMd5(Path.Combine(outPath, name)), File.ReadAllBytes(Path.Combine(outPath, name)).Length));
                    }
                    else
                    {
                        Debug.Log("路径有问题" + Path.Combine(outPath, name));
                        return;
                    }

                    //
                    if (File.Exists(Path.Combine(outPath, name + ".manifest")))
                    {
                        abInfo.Add(new AssetDesc(name + ".manifest", GetFileMd5(Path.Combine(outPath, name + ".manifest")), File.ReadAllBytes(Path.Combine(outPath, name + ".manifest")).Length));
                    }
                    else
                    {
                        Debug.Log("路径有问题" + Path.Combine(outPath, name + ".manifest"));
                        return;
                    }

                }

                //如何把List集合变成json数据
                var result = Newtonsoft.Json.JsonConvert.SerializeObject(abInfo, Newtonsoft.Json.Formatting.Indented);
                var resultPath = Path.Combine(outPath, "file.json");

                File.WriteAllText(resultPath, result);
                if(!Directory.Exists(Application.streamingAssetsPath))
                {
                    Directory.CreateDirectory(Application.streamingAssetsPath);        
                }
                File.WriteAllText(Path.Combine(Application.streamingAssetsPath, "file.json"), result);
                AssetDatabase.Refresh();//刷新unity文件夹
            }
            else
            {
                //弹出unity提示窗口 
                EditorUtility.DisplayDialog("提示", "生成失败,请确认已经选择路径", "理解");
            }
        }

        //打包完开始压缩ab包
        GUILayout.Space(5);//竖向间隔
        if (GUILayout.Button("开始压缩ab包"))
        {        
            //需要有压缩输出路径,以及打包的ab包
            if (zipPath != "")
            {
                //开始压缩
                myZip = new MyZip();
                myZip.Restart();
                myZip.ZipFloder(outPath, zipPath);               
                Debug.Log("压缩成功");
                AssetDatabase.Refresh();//刷新unity文件夹
            }   
        }


        GUILayout.EndVertical();//结束竖直方向排列
    }

    //设置ab包名  根据资源所在文件夹
    private static void SetAssetBundleName(DirectoryInfo directoryInfo)
    {
        DirectoryInfo[] directoryInfos = directoryInfo.GetDirectories();//获取当前目录的子目录(不包含孙子目录)
        Debug.Log(directoryInfos.Length);
        FileInfo[] fileInfos = directoryInfo.GetFiles();//获取目录中的文件,不包含子目录当中的文件  包含.meta文件
        Debug.Log(fileInfos.Length);
        for (int i = 0; i < directoryInfos.Length; i++)
        {
            SetAssetBundleName(directoryInfos[i]);
        }
        for (int i = 0; i < fileInfos.Length; i++)
        {
            FileInfo info = fileInfos[i];
            //根据扩展名来判断
            if (info.Extension != ".meta" && info.Extension != ".cs"&& info.Extension != ".lua")
            {
                string path = info.FullName.Replace(@"\", @"/").Replace(Application.dataPath.Replace(@"\", @"/"), "");
                path = "Assets" + path;
                Debug.Log(path);
                TODO  
                string temp = resPath.Substring(resPath.LastIndexOf("/")+1);
                Debug.Log(temp);
                string assetbundleName = path.Replace("Assets/"+ temp + "/", "").Replace(info.Extension, "");
                Debug.Log(assetbundleName);
                //string assetbundleName = path.Replace("Assets/Prefabs" + "/", "").Replace(info.Extension, "");
                AssetImporter assetImporter = AssetImporter.GetAtPath(path);
                assetImporter.assetBundleName = assetbundleName;
            }
        }
    }

    //清除ab包名
    public static void ClearAssetBundleName(DirectoryInfo directoryInfo)
    {
        DirectoryInfo[] directoryInfos = directoryInfo.GetDirectories();
        FileInfo[] fileInfos = directoryInfo.GetFiles();
        for (int i = 0; i < directoryInfos.Length; i++)
        {
            ClearAssetBundleName(directoryInfos[i]);
        }
        for (int i = 0; i < fileInfos.Length; i++)
        {
            if (!fileInfos[i].Extension.Contains(".meta") && !fileInfos[i].Extension.Contains(".cs"))
            {
                string path = fileInfos[i].FullName.Replace(@"\", @"/").Replace(Application.dataPath.Replace(@"\", @"/"), "");
                path = "Assets" + path;
                AssetImporter assetImporter = AssetImporter.GetAtPath(path);
                assetImporter.assetBundleName = string.Empty;
            }
        }
    }

    //获取文件MD5值
    public static string GetFileMd5(string filename)
    {
        //string  filename = Application.dataPath+"/Test/CompressZip.zip";
        string filemd5 = null;
        try
        {
            using (var fileStream = File.OpenRead(filename))
            {
                var md5 = MD5.Create();
                var fileMD5Bytes = md5.ComputeHash(fileStream);//计算指定Stream 对象的哈希值                                     
                filemd5 = FormatMD5(fileMD5Bytes);
            }
        }
        catch (System.Exception ex)
        {
            Debug.Log(ex);
        }
        return filemd5;
    }
    static string FormatMD5(Byte[] data)
    {
        return System.BitConverter.ToString(data).Replace("-", "").ToLower();//将byte[]装换成字符串
    }

    
}




public class AssetDesc
{
    public AssetDesc(string abname, string md5, int size)
    {
        this.abname = abname;
        this.md5 = md5;
        this.size = size;
    }

    public string abname;
    public string md5;
    public int size;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using ICSharpCode.SharpZipLib.Zip;
using System.IO;
using System.Threading;
using ICSharpCode.SharpZipLib.Core;
using System;
//https://blog.csdn.net/hiramtan/article/details/49902359
//unity如何解压压缩包,压缩文件夹
//添加递归方式压缩/解压缩文件夹
//重要
//1.文件/文件夹不支持中文
//2.会阻塞主线程
public class MyZip : MonoBehaviour
{


    void Start()
    {
        //测试压缩
        //ZipFloder(Application.dataPath+"/CompressZip", Application.dataPath + "/Test/CompressZip.zip");

        //测试解压
       UnZipFile(Application.dataPath + "/Test/CompressZip.zip", Application.dataPath + "/Scenes");
    }
    //进度刷新时间
    public float progressUpdateTime = 0.2f;

    //每个文件的压缩/解压进度
    public float progress { private set; get; }

    //文件的压缩/解压总进度
    public float progressOverall { private set; get; }



    public void Restart()
    {
        if(thread!=null)
            thread.Abort();
        thread = null;
    }

    public void StopThread()
    {
        thread.Abort();
    }

    Thread thread;
    /// <summary>
    /// 压缩文件夹   应当在编辑器模式下执行
    /// </summary>
    /// <param name="_fileFolder">文件夹路径</param>
    /// <param name="_outZip">zip文件路径+名字</param>
    public void ZipFloder(string _fileFolder, string _outZip)
    {
        string directory = _outZip.Substring(0, _outZip.LastIndexOf("/"));//测一下
        if (!Directory.Exists(directory))
            Directory.CreateDirectory(directory);
        if (File.Exists(_outZip))
            File.Delete(_outZip);
        progress = progressOverall = 0;
         thread = new Thread(delegate ()
        {
            int fileCount = FileCount(_fileFolder);//文件总数
            int fileCompleted = 0;
            FastZipEvents events = new FastZipEvents();
            events.Progress = new ProgressHandler((object sender, ProgressEventArgs e) =>
            {
                progress = e.PercentComplete;
                if (progress == 100)
                {
                    fileCompleted++;
                    progressOverall = 100 * fileCompleted / fileCount;
                }
            });
            events.ProgressInterval = TimeSpan.FromSeconds(progressUpdateTime);
            events.ProcessFile = new ProcessFileHandler( (object sender, ScanEventArgs e) => { });           
            //压缩
            FastZip fastZip = new FastZip(events);
            fastZip.CreateZip(_outZip, _fileFolder, true, "");

            
        });
        thread.IsBackground = true;
        thread.Start();
      
    }

    /// <summary>
    /// 解压zip文件  运行时执行
    /// </summary>
    /// <param name="_zipFIle">需要解压的zip路径和名字</param>
    /// <param name="_outFolder">解压路径</param>
    public void UnZipFile(string _zipFile,string _outFolder)
    {
        //如果解压之前存在同名文件,解压之后会直接替换旧文件
        if (!Directory.Exists(_outFolder))           
            Directory.CreateDirectory(_outFolder);


        progress = progressOverall = 0;

         thread = new Thread(delegate ()
         {
             int fileCount = (int)new ZipFile(_zipFile).Count;
             int fileCompleted = 0;
             FastZipEvents events = new FastZipEvents();
             events.Progress = new ProgressHandler((object sender, ProgressEventArgs e) =>
              {
                  progress = e.PercentComplete;
                  if (progress == 100)
                  {
                      fileCompleted++;
                      progressOverall = 100 * fileCompleted / fileCount;
                  }
              });

             events.ProgressInterval = TimeSpan.FromSeconds(progressUpdateTime);
             events.ProcessFile = new ProcessFileHandler((object sender, ScanEventArgs e) => { });

             //解压
             FastZip fastZip = new FastZip(events);
             fastZip.ExtractZip(_zipFile, _outFolder, "");
         });
        thread.IsBackground = true;
        thread.Start();
        
    }

    //https://github.com/hiramtan/HiZip_unity
    private int FileCount(string path)
    {
        int result = Directory.GetFiles(path).Length;
        string[] subFolders = Directory.GetDirectories(path);
        foreach (string subFolder in subFolders)
        {
            result += FileCount(subFolder);
        }
        return result;
    }


}
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;

//如何从服务器下载资源
public class DownLoadFile : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine(SaveFile(@"D:\Test\New\version.txt"));
        StartCoroutine(SaveFile(@"D:\Test\New\MainBGM.mp3"));
        StartCoroutine(SaveFile(@"D:\Test\New\New.rar"));
        StartCoroutine(SaveFile(@"D:\Test\New\1.rar"));
        StartCoroutine(SaveFile(@"D:\Test\New\11.rar"));
        StartCoroutine(SaveFile(@"D:\Test\New\111.rar"));
        StartCoroutine(SaveFile(@"D:\Test\New\1111.rar"));
    }

    public  float LoadProcess;
    IEnumerator SaveFile(string path)
    {
        while (!Caching.ready)
        {
            yield return null;
        }
        using (UnityWebRequest uwr=UnityWebRequest.Get(path))
        {
            yield return uwr.SendWebRequest();
            if (uwr.error!=null)
            {
                throw new Exception("www download had an error" + uwr.error);
            }
            LoadProcess = uwr.downloadProgress;
            print(LoadProcess);
            if (uwr.isDone)
            {
                byte[] results = uwr.downloadHandler.data;
             
                FileInfo fileInfo = new FileInfo(Application.dataPath + "/" + path.Substring(path.LastIndexOf(@"\")+1));
                FileStream fs = fileInfo.Create();
                //fs.Write(字节数组, 开始位置, 数据长度);
                fs.Write(uwr.downloadHandler.data, 0, uwr.downloadHandler.data.Length);
                fs.Flush();     //文件写入存储到硬盘
                fs.Close();     //关闭文件流对象
                fs.Dispose();   //销毁文件对象
            }
        }
       
    }
    // Update is called once per frame
    void Update()
    {
        
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

//接下来结合lua实现热更新效果
public class Test : MonoBehaviour
{
    MyZip myZip;
    bool isLoadScene = true;
    // Start is called before the first frame update
    void Start()
    {
        //解压资源
        //myZip = new MyZip();
        //myZip.UnZipFile(Application.dataPath + "/Scenes/assetbundles.zip", Application.dataPath + "/Scenet");
        //从服务器下载资源到本地
        
    }
  
    // Update is called once per frame
    void Update()
    {
        //unity打包ab包到更新整个流程
        //第一步判断Application.persistentDataPath里面有没有对应ab包文件
        //如果没有就是第一次打开应用才会如此第一步拷贝Application.streamingAssetsPath里面的ab包压缩包到目录Application.persistentDataPath,然后解压
        //如果有就不需要解压
        //从服务器下载版本号和file.json文件MD5
        //先判断版本号是否一致
        //若不一致  下载服务器ab包压缩包 下载file.json文件转换为对应list
        //判断文件的MD5是否有变化   用list存储md5有变化的ab包  得到需要更新的资源列表
        //根据更新资源列表下载资源   问题下载了同名文件会怎么处理
        //下载完毕,更换版本和file.json文件
        //开始加载执行lua代码
        //如果一致  就没事直接加载lua代码



        判断是否解压完毕从服务器下载的资源
        if (myZip.progressOverall>=100&& isLoadScene)
        {
            myZip.StopThread();
            //Debug.Log(File.ReadAllText(Application.dataPath + "/Scenet/file.json"));
            isLoadScene = false;

            //因为已经下载好了,只需要从本地ab包加载资源就可以了
            AssetBundle ab = AssetBundle.LoadFromFile(Application.dataPath + "/Scenet/Cube");
            GameObject go= ab.LoadAsset<GameObject>("Cube");
            GameObject cube = Instantiate<GameObject>(go);
            cube.transform.localPosition = Vector3.zero;

            //StartCoroutine(LoadWWW(Application.dataPath + "/Scenet/Cube"));
        }
        
    }

    //从ab包加载资源
    private WWW Loadab;
    IEnumerator LoadWWW(string path)
    {
        Loadab = new WWW(path);
        yield return Loadab;
        if (Loadab.error!=null)
        {
            Debug.Log(Loadab.error);
        }
        else
        {
            GameObject go = Loadab.assetBundle.LoadAsset<GameObject>("Cube");
            GameObject cube = Instantiate<GameObject>(go);
            cube.transform.localPosition = Vector3.zero;
        }
    }


}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值