c# Ftp/Http资源上传下载(支持断点续传) + IIS http/ftp服务器

Win11系统,使用c#控制台应用程序

1. IIS资源服务器部署

网上有很多教程这里提供一些教程,比我写的好

  1. 什么是IIS
  2. 开启IIS+部署 1
  3. 开启IIS+部署 2
  4. 开启IIS+部署 3
  5. 如何查看ip
  6. 如何查看被占用的端口号
  7. 防火墙限制,防火墙设置起来比较麻烦,可以直接关闭防火墙
  8. 以上教程基本上能部署好资源服务器,配置过程中,尽量不要设置用户和密码,以及ftp配置SSL验证,不然问题还是比较多的

2. Http资源下载,支持断点续传

由于http服务器没有研究明白,调资源上传功能一直没有调通,可以使用Ftp上传代替,网上有很多布置在资源服务器上的上传代码可以参考,整个网页上茶资源也不错

脚本名称: HttpFileUtil.cs

2.1 获取资源下载大小

/// <summary>
/// 获取文件大小
/// </summary>
/// <param name="url"></param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <returns></returns>
public static long GetDownloadFileSize(string url, string userName = "", string password = "")
{
    try
    {
    	url = HttpUtility.UrlPathEncode(url);
        var req = (HttpWebRequest)WebRequest.Create(url);
        req.Method = WebRequestMethods.Http.Head;
        req.Credentials = new NetworkCredential(userName, password);
        req.Timeout = 1000;
        req.Credentials = new NetworkCredential(userName, password);
        var res = (HttpWebResponse)req.GetResponse();

        long rl = 0;
        if (res.StatusCode == HttpStatusCode.OK)
        {
            rl = res.ContentLength;
        }
        res.Close();
        return rl;

    }
    catch { }

    return 0;
}

2.1 资源下载,并且支持断点续传

/// <summary>
/// 下载文件(支持断点续下)
/// </summary>
/// <param name="url"></param>
/// <param name="desDir">本地文件夹路径</param>
/// <param name="overwrite">覆盖下载</param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <param name="callback">进度回调</param>
/// <returns></returns>
public static bool DownloadFile(string url, string desDir,bool overwrite = false, string userName = "", string password = "",Action<long,long> callback = null)
{
    string fileName = Path.GetFileName(url);
    if (string.IsNullOrEmpty(desDir))
    {
        //路径为空,设置文件名
        desDir = fileName;
    }
    else
    {
        if (!Directory.Exists(desDir))
        {
            //文件夹不存在,创建新文件夹
            Directory.CreateDirectory(desDir);
        }

        //路径连接
        desDir = Path.Combine(desDir, fileName);
    }

    //编码Url
    url = HttpUtility.UrlPathEncode(url);
    long downloadFileSize = GetDownloadFileSize(url, userName, password);
    if (downloadFileSize <= 0)
    {
        //下载文件异常
        callback?.Invoke(-1, -1);
        return false;
    }

    bool flag = false;
    long curPosition = 0;
    Stream stream = null;
    FileStream fileStream = null;
    //下载未完成之前存为临时文件(支持断点续下)
    string tmpFile = desDir + ".temp";

    try
    {
        //下载文件已存在
        if (File.Exists(desDir))
        {
            var fileInfo = new FileInfo(desDir);
            curPosition = fileInfo.Length;
            if (curPosition >= downloadFileSize)
            {
                //覆盖进行删除重新下载
                if (overwrite)
                {
                    File.Delete(desDir);
                }
                else
                {
                    callback?.Invoke(downloadFileSize, downloadFileSize);
                    return true;
                }
            }
        }
    }
    catch
    {

    }

    try
    {
        //临时文件存在
        if (File.Exists(tmpFile))
        {
            fileStream = File.OpenWrite(tmpFile);
            curPosition = fileStream.Position;

            if (curPosition >= downloadFileSize)
            {
                //不覆盖
                if (!overwrite)
                {
                    if (fileStream != null)
                    {
                        fileStream.Close();
                        fileStream.Dispose();
                    }
                    //重命名
                    File.Move(tmpFile, desDir);
                    callback?.Invoke(downloadFileSize, downloadFileSize);
                    return true;
                }
                curPosition = 0;
            }

            callback?.Invoke(curPosition, downloadFileSize);
            fileStream.Seek(curPosition, SeekOrigin.Current);
        }
        else
        {
            fileStream = new FileStream(tmpFile, FileMode.Create, FileAccess.Write);
            curPosition = 0;
        }

        HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(url);
        httpWebRequest.Credentials = new NetworkCredential(userName, password);
        if (curPosition > 0)
        {
            //断点续下,将请求下载的字节流下载位置设置成上次下载位置
            httpWebRequest.AddRange(curPosition);
        }
        long downloadSize = curPosition;
        var res = httpWebRequest.GetResponse();
        string totalLength = FormatSize(res.ContentLength, 2);
        stream = res.GetResponseStream();
        byte[] buffer = new byte[4096];
        int byteReader = 0;
        do
        {
            //读取, 写入本地
            byteReader = stream.Read(buffer, 0, buffer.Length);
            downloadSize += byteReader;
            callback?.Invoke(curPosition, downloadFileSize);
            fileStream.Write(buffer, 0, byteReader);
        }
        while (byteReader != 0);

        //下载完成
        callback?.Invoke(downloadFileSize, downloadFileSize);
        res.Close();
        flag = true;
    }
    catch (Exception e)
    {
        callback?.Invoke(-1, -1);
    }
    finally
    {
        if (fileStream != null)
        {
            fileStream.Flush();
            fileStream.Close();
            fileStream.Dispose();
        }
        if (stream != null)
        {
            stream.Dispose();
            stream.Close();
        }
        if (flag)
        {
          //临时文件重命名
            File.Move(tmpFile, desDir);
        }
    }

    return flag;
}

3. Ftp资源上传下载,支持断点续传

Ftp实现功能包含资源: 上传(支持断点续传), 下载(支持断点续下) , 修改名称,删除; 文件夹:创建,删除,修改名称,也是根据网上的案例惊醒修改的
脚本名称: HttpFileUtil.cs

3.1 检测ftp是否可连接

/// <summary>
/// 检测ftp是否登录
/// </summary>
/// <param name="url"></param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <returns></returns>
public static bool CheckFtp(string url, string userName = "", string password = "")
{
    try
    {
        //编码Url
        url = HttpUtility.UrlPathEncode(url);
        FtpWebRequest request = (FtpWebRequest)FtpWebRequest.Create(url);
        request.Method = WebRequestMethods.Ftp.ListDirectory;
        request.Credentials = new NetworkCredential(userName, password);
        request.Timeout = 3000;//3秒超时

        FtpWebResponse ftpWebResponse = (FtpWebResponse)request.GetResponse();
        Stream responseStream = ftpWebResponse.GetResponseStream();

        responseStream.Close();
        ftpWebResponse.Close();

        return true;
    }
    catch (Exception ex)
    {
        return false;
    }
}

3.2 获取文件大小

/// <summary>
/// 获取文件大小
/// </summary>
/// <param name="url"></param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <returns></returns>
public static long GetDownloadFileSize(string url, string userName = "", string password = "")
{
    try
    {
        url = HttpUtility.UrlPathEncode(url);
        FtpWebRequest ftpWebRequest = (FtpWebRequest)FtpWebRequest.Create(url);
        ftpWebRequest.Credentials = new NetworkCredential(userName, password);
        ftpWebRequest.Method = WebRequestMethods.Ftp.GetFileSize;
        ftpWebRequest.UseBinary = true;
        FtpWebResponse ftpWebResponse = (FtpWebResponse)ftpWebRequest.GetResponse();
        Stream stream = ftpWebResponse.GetResponseStream();
        long fileSize = ftpWebResponse.ContentLength;
        stream.Close();
        ftpWebResponse.Close();
        return fileSize;
    }
    catch (Exception ex)
    {
    }
    return 0;
}

3.3 创建文件夹,支持多级目录创建

创建文件夹包含,一级文件夹和多级文件夹,针对多级文件夹的,需要逐级文件夹进行创建

/// <summary>
/// 创建文件夹
/// </summary>
/// <param name="url"></param>
/// <param name="newDir"></param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <returns></returns>
public static bool MaskDirectory(string url, string newDir, string userName = "", string password = "")
{
    //创建新文件夹为空
    if (string.IsNullOrEmpty(newDir))
    {
        return true;
    }

    string targetUrl = HttpUtility.UrlPathEncode(url + "/" + newDir);
    //检测远端文件夹是否存在
    bool check = CheckFtp(targetUrl, userName, password);
    if (check)
    {
        //存在
        return true;
    }

    //多级文件夹处理(例如:x/xx/xxx/xxxx)
    string[] dirs = newDir.Replace("\\", "/").Split('/');
    int length = dirs.Length;
    if (length <= 1)
    {
        //一级文件夹,直接创建
        return _MaskDirectory(url, targetUrl, userName, password);
    }

    //多级文件夹,分级创建,
    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < length; i++)
    {
        if (string.IsNullOrEmpty(dirs[i]))
        {
            continue;
        }

        stringBuilder.Append(dirs[i]);
        targetUrl = stringBuilder.ToString();
        //检查当前文件夹是否存在
        check = CheckFtp(url + "/" + targetUrl, userName, password);
        if (check)
        {
            continue;
        }

        //进行创建
        bool flag = _MaskDirectory(url, targetUrl, userName, password);
        if (!flag)
        {
            return false;
        }

        stringBuilder.Append("/");
    }

    return true;
}

上面是处理多级文件夹,下面是ftp新建文件夹实现

/// <summary>
/// 创建文件夹
/// </summary>
/// <param name="url"></param>
/// <param name="newDir">新文件夹</param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <returns></returns>
private static bool _MaskDirectory(string url, string newDir, string userName = "", string password = "")
{
    if (string.IsNullOrEmpty(newDir))
    {
        return true;
    }
    try
    {
        url = HttpUtility.UrlPathEncode(url + "/" + newDir);
        FtpWebRequest ftpWebRequest = (FtpWebRequest)FtpWebRequest.Create(url);
        ftpWebRequest.Method = WebRequestMethods.Ftp.MakeDirectory;
        ftpWebRequest.KeepAlive = false;
        ftpWebRequest.UseBinary = true;
        ftpWebRequest.Credentials = new NetworkCredential(userName, password);

        FtpWebResponse ftpWebResponse = (FtpWebResponse)ftpWebRequest.GetResponse();
        Stream stream = ftpWebResponse.GetResponseStream();

        stream.Close();
        ftpWebResponse.Close();
        return true;
    }
    catch (Exception ex)
    {
    }
    return false;
}

3.4 删除文件夹

和创建文件一样,删除文件夹也包含,一级文件夹和多级文件夹,针对多级文件删除,需要从最内层向外逐级删除,
特别提醒 : 删除的文件夹是指空文件夹,多级文件夹,除了最内层每层除了包含待删除的不能包含其他的文件(夹),确保服务器安全,ftp就是这种机制
在这里插入图片描述
如上图,ftp服务器所指定的物理路径为:D:\ServerFtpRoot,资源上传,下载,删除,改名,以及文件夹创建删除等操作,都是在这个目录下进行的, 多级文件夹路径:/1/2/3/4/5,下面删除操作结果:

  1. 删除5: 5为空目录则删除成功,否则删除失败
  2. 删除4/5: 先操作第一步,5删除成功后,4为空目录则删除成功,否则删除失败
  3. 删除3/4/5: 先执行前两步,判断3是否为空文件夹,空文件夹才能删除,否则删除失败
  4. 删除2/3/4: 无法删除,是因为4为非空文件夹,还有5,所以2/3/4是不能删除的
  5. 删除2/3/4/5: 在4的文件夹下还有一个6(为文件或文件夹),执行结果: 成功删除了5,2/3/4是无法删除的
  6. 删除1/2/3/4/5: 若1,2,3,4,5都为空文件夹,执行后,1/2/3/4/5都会被删除
/// <summary>
/// 删除文件夹
/// </summary>
/// <param name="url"></param>
/// <param name="delDir">文件夹</param>
/// <param name="removeDir">删除子文件夹,为了安全禁止删除文件夹时删除文件以及其他子目录</param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <returns></returns>
public static bool RemoveDirectory(string url, string delDir, bool removeDir = false, string userName = "", string password = "")
{
    //删除远端path路径的文件夹
    bool remove = _RemoveDirectory(url, delDir, userName, password);
    if (!removeDir || !remove)
    {
        return remove;
    }

    //当path为多级文件夹时,需要依次删除
    string[] dirs = delDir.Replace("\\", "/").Split('/');
    int length = dirs.Length;
    if (length <= 1)
    {
        return false;
    }

    for (int i = length - 2; i >= 0; i--)
    {
        if (string.IsNullOrEmpty(dirs[i]))
        {
            continue;
        }
        delDir = delDir.Substring(0, delDir.Length - 1 - dirs[i].Length);
        remove = _RemoveDirectory(url, delDir, userName, password);
        if (!remove)
        {
            return false;
        }
    }

    return true;
}

上面是处理多级文件夹,下面是ftp删除实现

/// <summary>
/// 传出文件夹
/// </summary>
/// <param name="url"></param>
/// <param name="delDir"></param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <returns></returns>
private static bool _RemoveDirectory(string url, string delDir, string userName = "", string password = "")
{
    try
    {
        url = HttpUtility.UrlPathEncode(url + "/" + delDir);
        FtpWebRequest ftpWebRequest = (FtpWebRequest)FtpWebRequest.Create(url);
        ftpWebRequest.Method = WebRequestMethods.Ftp.RemoveDirectory;
        ftpWebRequest.KeepAlive = false;
        ftpWebRequest.Credentials = new NetworkCredential(userName, password);

        FtpWebResponse ftpWebResponse = (FtpWebResponse)ftpWebRequest.GetResponse();
        Stream stream = ftpWebResponse.GetResponseStream();
        
        stream.Close();
        ftpWebResponse.Close();
        return true;
    }
    catch (Exception e)
    {
    }
    return false;
}

3.5 文件上传(支持断点续传)

资源上传,若指定了ftp相对存储位置,也就是remotePath参数,分析了是否为多级文件夹的处理

/// <summary>
/// 上传文件
/// </summary>
/// <param name="url">ftp地址</param>
/// <param name="localfile">本地文件名</param>
/// <param name="remotePath">远传的文件名</param>
/// <param name="overwrite">是否覆盖远端文件</param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <param name="callback">进度回调</param>
public static bool Upload(string url, string localfile, string remotePath = "", bool overwrite = true, string userName = "", string password = "", Action<long, long> callback = null)
{
    try
    {
        string oldUrl = url;
        //获取文件名
        string fileName = Path.GetFileName(localfile);
        if (!string.IsNullOrEmpty(remotePath))
        {
            //路径连接
            fileName = Path.Combine(remotePath, fileName);
        }
        //编码Url
        url = HttpUtility.UrlPathEncode(url + "/" + fileName);

        //获取远端文件夹路径
        string dir = Path.GetDirectoryName(fileName);
        //进行检测远端文件夹是否存在
        bool check = CheckFtp(oldUrl + "/" + dir);
        if (!check)
        {
            //不存在进行创建
            check = MaskDirectory(oldUrl, dir, userName, password);
            if (!check)
            {
                callback?.Invoke(-1, -1);
                return false;
            }
        }

        //获取远端文件大小(支持断点续传)
        long uploadSize = GetDownloadFileSize(url);
        //本地文件大小
        FileInfo fileInfo = new FileInfo(localfile);
        long totalSize = fileInfo.Length;
        //远端文件大小与本地文件大小对比
        if (uploadSize >= totalSize)
        {
            if (overwrite)
            {
                //覆盖,重新上传
                uploadSize = 0;
            }
            else
            {
                //不覆盖,停止上传
                callback?.Invoke(totalSize, totalSize);
                return true;
            }
        }

        FtpWebRequest request = (FtpWebRequest)FtpWebRequest.Create(url);
        request.Credentials = new NetworkCredential(userName, password);
        if (uploadSize > 0)
        {
            //断点续传
            request.Method = WebRequestMethods.Ftp.AppendFile;
        }
        else
        {
            //上传
            request.Method = WebRequestMethods.Ftp.UploadFile;
        }
        request.KeepAlive = false;//
        request.UseBinary = true; //
        //需上传大小
        request.ContentLength = fileInfo.Length;

        //读写块大小 4096B = 4k
        byte[] buffer = new byte[4096];
        int bytesRead = 0;
        FileStream fileStream = fileInfo.OpenRead();
        Stream stream = request.GetRequestStream();

        if (uploadSize > 0)
        {
            //断点续传,将本地文件字节流读取位置设置到上次上传位置
            fileStream.Seek(uploadSize, SeekOrigin.Begin);
        }

        do
        {
            //读写,上传
            bytesRead = fileStream.Read(buffer, 0, buffer.Length);
            uploadSize += bytesRead;
            stream.Write(buffer, 0, bytesRead);
            callback?.Invoke(uploadSize, totalSize);
        }
        while (bytesRead != 0);
        //上传完毕
        callback?.Invoke(totalSize, totalSize);

        //关闭字节流
        stream.Close();
        //关闭文件流
        fileStream.Close();
        //关闭链请求

        return true;
    }
    catch (Exception ex)
    {
        callback?.Invoke(-1, -1);
    }
    return false;
}

3.6 文件下载(支持断点续下)

 /// <summary>
 /// 下载文件(支持断点续下)
 /// </summary>
 /// <param name="url"></param>
 /// <param name="desFile">本地文件夹路径</param>
 /// <param name="userName">账号</param>
 /// <param name="password">密码</param>
 /// <returns></returns>
 public static bool Download(string url, string desDir, bool overwrite = true, string userName = "", string password = "", Action<long, long> callback = null)
 {
     string fileName = Path.GetFileName(url);
     if (string.IsNullOrEmpty(desDir))
     {
         //路径为空,设置文件名
         desDir = fileName;
     }
     else
     {
         if (!Directory.Exists(desDir))
         {
             //文件夹不存在,创建新文件夹
             Directory.CreateDirectory(desDir);
         }

         //路径连接
         desDir = Path.Combine(desDir, fileName);
     }

     url = HttpUtility.UrlPathEncode(url);

     //下载文件大小
     long downloadFileSize = GetDownloadFileSize(url, userName, password);
     if (downloadFileSize <= 0)
     {
         //下载文件异常
         callback?.Invoke(-1, -1);
         return false;
     }

     bool flag = false;
     long curPosition = 0;
     Stream stream = null;
     FileStream fileStream = null;
     //下载未完成之前存为临时文件(支持断点续下)
     string tmpFile = fileName + ".temp";

     try
     {
         //下载文件已存在
         if (File.Exists(desDir))
         {
             var fileInfo = new FileInfo(desDir);
             curPosition = fileInfo.Length;
             if (curPosition >= downloadFileSize)
             {
                 //覆盖进行删除重新下载
                 if(overwrite)
                 {
                     File.Delete(desDir);
                 }
                 else
                 {
                     callback?.Invoke(downloadFileSize, downloadFileSize);
                     return true;
                 }
             }
         }
     }
     catch { }

     try
     {
         //临时文件存在
         if (File.Exists(tmpFile))
         {
             fileStream = File.OpenWrite(tmpFile);
             curPosition = fileStream.Length;
             //远端文件大小与临时文件大小对比
             if (curPosition >= downloadFileSize)
             {
                 //不覆盖
                 if(!overwrite)
                 {
                     if (fileStream != null)
                     {
                         fileStream.Close();
                         fileStream.Dispose();
                     }
                     //重命名
                     File.Move(tmpFile, desDir);
                     callback?.Invoke(downloadFileSize, downloadFileSize);
                     return true;
                 }
                 curPosition = 0;
             }

             callback?.Invoke(curPosition, downloadFileSize);
             //将本地文件字节流写入位置设置成上次下载位置
             fileStream.Seek(curPosition, SeekOrigin.Current);
         }
         else
         {
             fileStream = new FileStream(tmpFile, FileMode.Create, FileAccess.Write);
             curPosition = 0;
         }

         FtpWebRequest ftpWebRequest = (FtpWebRequest)FtpWebRequest.Create(url);
         ftpWebRequest.Credentials = new NetworkCredential(userName, password);
         ftpWebRequest.Method = WebRequestMethods.Ftp.DownloadFile;
         ftpWebRequest.UseBinary = true;
         if (curPosition > 0)
         {
             //断点续下,将请求下载的字节流下载位置设置成上次下载位置
             ftpWebRequest.ContentOffset = curPosition;
         }

         FtpWebResponse ftpWebResponse = (FtpWebResponse)ftpWebRequest.GetResponse();

         stream = ftpWebResponse.GetResponseStream();

         byte[] buffer = new byte[4096];
         int bytesRead = 0;
         do
         {
             //读取, 写入本地
             bytesRead = stream.Read(buffer, 0, buffer.Length);
             curPosition += bytesRead;
             callback?.Invoke(curPosition, downloadFileSize);
             fileStream.Write(buffer, 0, bytesRead);
         }
         while (bytesRead != 0);

         //下载完成
         callback?.Invoke(downloadFileSize, downloadFileSize);
         ftpWebResponse.Close();
         flag = true;

     }
     catch (Exception e)
     {
         callback?.Invoke(-1, -1);
     }
     finally
     {
         if (stream != null)
         {
             stream.Close();
             stream.Dispose();
         }
         if (fileStream != null)
         {
             fileStream.Flush();
             fileStream.Close();
             fileStream.Dispose();
         }
         if (flag)
         {
             File.Move(tmpFile, desDir);
         }
     }

     return flag;
 }

3.7 文件删除

       /// <summary>
       /// 删除文件
       /// </summary>
       /// <param name="url"></param>
       /// <param name="userName">账号</param>
       /// <param name="password">密码</param>
       /// <returns></returns>
       public static bool Delete(string url, string userName = "", string password = "")
       {
           try
           {
               url = HttpUtility.UrlPathEncode(url);
               FtpWebRequest ftpWebRequest = (FtpWebRequest)FtpWebRequest.Create(url);
               ftpWebRequest.Method = WebRequestMethods.Ftp.DeleteFile;
               ftpWebRequest.KeepAlive = false;
               ftpWebRequest.Credentials = new NetworkCredential(userName, password);

               FtpWebResponse ftpWebResponse = (FtpWebResponse)ftpWebRequest.GetResponse();
               Stream stream = ftpWebResponse.GetResponseStream();

               stream.Close();
               ftpWebResponse.Close();
               return true;
           }
           catch (Exception ex)
           {
           }
           return false;
       }

3.8 重命名

/// <summary>
/// 重命名
/// </summary>
/// <param name="url"></param>
/// <param name="newName">新名称,涉及到多级文件夹(未实现)</param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
/// <returns></returns>
public static bool Rename(string url, string newName, string userName = "", string password = "")
{
    try
    {
        url = HttpUtility.UrlPathEncode(url);
        FtpWebRequest ftpWebRequest = (FtpWebRequest)FtpWebRequest.Create(url);
        ftpWebRequest.Method = WebRequestMethods.Ftp.Rename;
        ftpWebRequest.RenameTo = newName;
        ftpWebRequest.KeepAlive = false;
        ftpWebRequest.Credentials = new NetworkCredential(userName, password);

        FtpWebResponse ftpWebResponse = (FtpWebResponse)ftpWebRequest.GetResponse();
        Stream stream = ftpWebResponse.GetResponseStream();

        stream.Close();
        ftpWebResponse.Close();
        return true;
    }
    catch (Exception ex)
    {
    }
    return false;
}

3.9 移动文件

/// <summary>
/// 移动文件夹
/// </summary>
/// <param name="url"></param>
/// <param name="newName"></param>
/// <param name="userName">账号</param>
/// <param name="password">密码</param>
public static void MoveFile(string url, string newName, string userName = "", string password = "")
{
    Rename(url, newName, userName, password);
}

上面为HttpFileUtil.cs和FtpFileUtil.cs两个文件的所有方法,除了重命名和移动未测试,其他都基本测过,都可使用,记录结束

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值