[WebApi] 捣鼓一个资源管理器--数据库辅助服务器文件访问

Dot NET 同时被 2 个专栏收录
22 篇文章 0 订阅

《打造一个网站或者其他网络应用的文件管理接口(WebApi)第四章“数据库辅助服务器文件访问”》

========================================================
作者:
qiujuer
博客:blog.csdn.net/qiujuer
网站:www.qiujuer.net
开源库:Genius-Android
转载请注明出处:http://blog.csdn.net/qiujuer/article/details/41744733
========================================================

History

[WebApi] 捣鼓一个资源管理器--文件下载
[WebApi] 捣鼓一个资源管理器--多文件上传
[WebApi] 捣鼓一个资源管理器--多文件上传+数据库辅助

In This

文件可以上传了,下一步就是文件的访问了;只有文件上传没有文件访问。那么数据就是浪费!

目标

  1. 文件访问要能通过上一章文件的那些东西访问磁盘中的文件
  2. 文件要能区分图片文件和其他文件;分别是直接打开和附件保存
  3. 文件要能有浏览器缓存,如打开同一个图片不用重复从服务器加载

分析

  1. 文件的访问这个当然要借助上一章中在数据库中保存的数据进行组合成文件返回流
  2. 直接打开和附件无非就是设置返回头中的Disposition:inline/attachment
  3. 浏览器缓存这个就是设置返回头中的缓存信息了

CodeTime

改动


这次简单,只更改了一个文件;不过更改较大。

流程


简单画了一个;凑合着看啊;就是一个请求来了,先查找数据库,找到该文件的信息;根据这些信息去查找文件。然后返回;没有找到数据 或者没有找到文件都直接返回404

Get()

        /// <summary>
        /// Get File
        /// </summary>
        /// <param name="name">MD5 Name</param>
        /// <returns>File</returns>
        [HttpGet]
        [Route("{Id}")]
        public async Task<HttpResponseMessage> Get(string Id)
        {
            // Return 304
            // 判断是否含有Tag,有就返回浏览器缓存
            var tag = Request.Headers.IfNoneMatch.FirstOrDefault();
            if (Request.Headers.IfModifiedSince.HasValue && tag != null && tag.Tag.Length > 0)
                return new HttpResponseMessage(HttpStatusCode.NotModified);

            //查找该文件
            Resource model = await db.Resources.FindAsync(Id);

            //未找到文件
            if (model == null)
                return new HttpResponseMessage(HttpStatusCode.BadRequest);

            // 加载文件信息
            FileInfo info = new FileInfo(Path.Combine(ROOT_PATH, model.Folder, model.Id));
            // 文件没有找到
            if (!info.Exists)
                return new HttpResponseMessage(HttpStatusCode.BadRequest);

            FileStream file = null;
            try
            {
                // 打开文件
                file = new FileStream(info.FullName, FileMode.Open, FileAccess.Read, FileShare.Read);
                // 新建http响应
                HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
                // 设置头部信息
                // GetDisposition() 用于决定文件是直接打开还是提示下载
                // 一般来说图片文件就直接打开,其他文件就提示下载
                ContentDispositionHeaderValue disposition = new ContentDispositionHeaderValue(GetDisposition(model.Type));
                // 设置文件名+扩展
                disposition.FileName = string.Format("{0}.{1}", model.Name, model.Type);
                // 文件名
                disposition.Name = model.Name;
                // 文件大小
                disposition.Size = file.Length;

                if (file.Length < MEMORY_SIZE)
                {
                    // 如果 小于64MB 就直接拷贝到内存流中返回
                    // Copy To Memory And Close.
                    byte[] bytes = new byte[file.Length];
                    await file.ReadAsync(bytes, 0, (int)file.Length);
                    file.Close();
                    MemoryStream ms = new MemoryStream(bytes);

                    result.Content = new ByteArrayContent(ms.ToArray());
                }
                else
                {
                    // 如果不是就直接打包为文件流返回
                    result.Content = new StreamContent(file);
                }

                // 设置文件在网络中的ContentType
                // GetContentType() 方法是根据扩展名去查找字典
                // 字典中我收集了大概150种,基本足够使用了
                result.Content.Headers.ContentType = new MediaTypeHeaderValue(GetContentType(model.Type));
                result.Content.Headers.ContentDisposition = disposition;

                // 设置浏览器缓存相关
                // 设置缓存Expires
                result.Content.Headers.Expires = new DateTimeOffset(DateTime.Now).AddHours(1);
                // 缓存最后修改时间
                result.Content.Headers.LastModified = new DateTimeOffset(model.Published);
                // 缓存控制
                result.Headers.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromHours(1) };
                // 设置ETag,这里的ETag为了方便就直接使用MD5值
                result.Headers.ETag = new EntityTagHeaderValue(string.Format("\"{0}\"", model.Id));

                // 返回请求
                return result;
            }
            catch
            {
                if (file != null)
                {
                    file.Close();
                }
            }

            return new HttpResponseMessage(HttpStatusCode.BadRequest);
        }
Get()方法是这次的精髓部分;其中的作用也都加上了注释了。

ResourceApiController.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Description;
using WebResource.Models;

namespace WebResource.Controllers
{
    [RoutePrefix("Res")]
    public class ResourceApiController : ApiController
    {
        // 操作 数据库
        private WebResourceContext db = new WebResourceContext();
        // 最大内存使用大小
        private static readonly long MEMORY_SIZE = 64 * 1024 * 1024;
        // 文件默认存储位置
        private static readonly string ROOT_PATH = HttpContext.Current.Server.MapPath("~/App_Data/");
        // ContentType字典
        private static Dictionary<string, string> CONTENT_TYPE = null;

        /// <summary>
        /// 初始化字典
        /// </summary>
        static ResourceApiController()
        {
            CONTENT_TYPE = new Dictionary<string, string>();
            CONTENT_TYPE.Add("ex", "application/andrew-inset");
            CONTENT_TYPE.Add("hqx", "application/mac-binhex40");
            CONTENT_TYPE.Add("cpt", "application/mac-compactpro");
            CONTENT_TYPE.Add("doc", "application/msword");
            CONTENT_TYPE.Add("docx", "application/msword");
            CONTENT_TYPE.Add("bin", "application/octet-stream");
            CONTENT_TYPE.Add("dms", "application/octet-stream");
            CONTENT_TYPE.Add("lha", "application/octet-stream");
            CONTENT_TYPE.Add("lzh", "application/octet-stream");
            CONTENT_TYPE.Add("exe", "application/octet-stream");
            CONTENT_TYPE.Add("class", "application/octet-stream");
            CONTENT_TYPE.Add("so", "application/octet-stream");
            CONTENT_TYPE.Add("dll", "application/octet-stream");
            CONTENT_TYPE.Add("oda", "application/oda");
            CONTENT_TYPE.Add("pdf", "application/pdf");
            CONTENT_TYPE.Add("ai", "application/postscript");
            CONTENT_TYPE.Add("eps", "application/postscript");
            CONTENT_TYPE.Add("ps", "application/postscript");
            CONTENT_TYPE.Add("smi", "application/smil");
            CONTENT_TYPE.Add("smil", "application/smil");
            CONTENT_TYPE.Add("mif", "application/vnd.mif");
            CONTENT_TYPE.Add("xls", "application/vnd.ms-excel");
            CONTENT_TYPE.Add("xlsx", "application/vnd.ms-excel");
            CONTENT_TYPE.Add("ppt", "application/vnd.ms-powerpoint");
            CONTENT_TYPE.Add("wbxml", "application/vnd.wap.wbxml");
            CONTENT_TYPE.Add("wmlc", "application/vnd.wap.wmlc");
            CONTENT_TYPE.Add("wmlsc", "application/vnd.wap.wmlscriptc");
            CONTENT_TYPE.Add("bcpio", "application/x-bcpio");
            CONTENT_TYPE.Add("vcd", "application/x-cdlink");
            CONTENT_TYPE.Add("pgn", "application/x-chess-pgn");
            CONTENT_TYPE.Add("cpio", "application/x-cpio");
            CONTENT_TYPE.Add("csh", "application/x-csh");
            CONTENT_TYPE.Add("dcr", "application/x-director");
            CONTENT_TYPE.Add("dir", "application/x-director");
            CONTENT_TYPE.Add("dxr", "application/x-director");
            CONTENT_TYPE.Add("dvi", "application/x-dvi");
            CONTENT_TYPE.Add("spl", "application/x-futuresplash");
            CONTENT_TYPE.Add("gtar", "application/x-gtar");
            CONTENT_TYPE.Add("hdf", "application/x-hdf");
            CONTENT_TYPE.Add("js", "application/x-javascript");
            CONTENT_TYPE.Add("skp", "application/x-koan");
            CONTENT_TYPE.Add("skd", "application/x-koan");
            CONTENT_TYPE.Add("skt", "application/x-koan");
            CONTENT_TYPE.Add("skm", "application/x-koan");
            CONTENT_TYPE.Add("latex", "application/x-latex");
            CONTENT_TYPE.Add("nc", "application/x-netcdf");
            CONTENT_TYPE.Add("cdf", "application/x-netcdf");
            CONTENT_TYPE.Add("sh", "application/x-sh");
            CONTENT_TYPE.Add("shar", "application/x-shar");
            CONTENT_TYPE.Add("swf", "application/x-shockwave-flash");
            CONTENT_TYPE.Add("flv", "application/x-shockwave-flash");
            CONTENT_TYPE.Add("sit", "application/x-stuffit");
            CONTENT_TYPE.Add("sv4cpio", "application/x-sv4cpio");
            CONTENT_TYPE.Add("sv4crc", "application/x-sv4crc");
            CONTENT_TYPE.Add("tar", "application/x-tar");
            CONTENT_TYPE.Add("tcl", "application/x-tcl");
            CONTENT_TYPE.Add("tex", "application/x-tex");
            CONTENT_TYPE.Add("texinfo", "application/x-texinfo");
            CONTENT_TYPE.Add("texi", "application/x-texinfo");
            CONTENT_TYPE.Add("t", "application/x-troff");
            CONTENT_TYPE.Add("tr", "application/x-troff");
            CONTENT_TYPE.Add("roff", "application/x-troff");
            CONTENT_TYPE.Add("man", "application/x-troff-man");
            CONTENT_TYPE.Add("me", "application/x-troff-me");
            CONTENT_TYPE.Add("ms", "application/x-troff-ms");
            CONTENT_TYPE.Add("ustar", "application/x-ustar");
            CONTENT_TYPE.Add("src", "application/x-wais-source");
            CONTENT_TYPE.Add("xhtml", "application/xhtml+xml");
            CONTENT_TYPE.Add("xht", "application/xhtml+xml");
            CONTENT_TYPE.Add("zip", "application/zip");
            CONTENT_TYPE.Add("rar", "application/zip");
            CONTENT_TYPE.Add("gz", "application/x-gzip");
            CONTENT_TYPE.Add("bz2", "application/x-bzip2");
            CONTENT_TYPE.Add("au", "audio/basic");
            CONTENT_TYPE.Add("snd", "audio/basic");
            CONTENT_TYPE.Add("mid", "audio/midi");
            CONTENT_TYPE.Add("midi", "audio/midi");
            CONTENT_TYPE.Add("kar", "audio/midi");
            CONTENT_TYPE.Add("mpga", "audio/mpeg");
            CONTENT_TYPE.Add("mp2", "audio/mpeg");
            CONTENT_TYPE.Add("mp3", "audio/mpeg");
            CONTENT_TYPE.Add("aif", "audio/x-aiff");
            CONTENT_TYPE.Add("aiff", "audio/x-aiff");
            CONTENT_TYPE.Add("aifc", "audio/x-aiff");
            CONTENT_TYPE.Add("m3u", "audio/x-mpegurl");
            CONTENT_TYPE.Add("rmm", "audio/x-pn-realaudio");
            CONTENT_TYPE.Add("rmvb", "audio/x-pn-realaudio");
            CONTENT_TYPE.Add("ram", "audio/x-pn-realaudio");
            CONTENT_TYPE.Add("rm", "audio/x-pn-realaudio");
            CONTENT_TYPE.Add("rpm", "audio/x-pn-realaudio-plugin");
            CONTENT_TYPE.Add("ra", "audio/x-realaudio");
            CONTENT_TYPE.Add("wav", "audio/x-wav");
            CONTENT_TYPE.Add("wma", "audio/x-wma");
            CONTENT_TYPE.Add("pdb", "chemical/x-pdb");
            CONTENT_TYPE.Add("xyz", "chemical/x-xyz");
            CONTENT_TYPE.Add("bmp", "image/bmp");
            CONTENT_TYPE.Add("gif", "image/gif");
            CONTENT_TYPE.Add("ief", "image/ief");
            CONTENT_TYPE.Add("jpeg", "image/jpeg");
            CONTENT_TYPE.Add("jpg", "image/jpeg");
            CONTENT_TYPE.Add("jpe", "image/jpeg");
            CONTENT_TYPE.Add("png", "image/png");
            CONTENT_TYPE.Add("tiff", "image/tiff");
            CONTENT_TYPE.Add("tif", "image/tiff");
            CONTENT_TYPE.Add("djvu", "image/vnd.djvu");
            CONTENT_TYPE.Add("djv", "image/vnd.djvu");
            CONTENT_TYPE.Add("wbmp", "image/vnd.wap.wbmp");
            CONTENT_TYPE.Add("ras", "image/x-cmu-raster");
            CONTENT_TYPE.Add("pnm", "image/x-portable-anymap");
            CONTENT_TYPE.Add("pbm", "image/x-portable-bitmap");
            CONTENT_TYPE.Add("pgm", "image/x-portable-graymap");
            CONTENT_TYPE.Add("ppm", "image/x-portable-pixmap");
            CONTENT_TYPE.Add("rgb", "image/x-rgb");
            CONTENT_TYPE.Add("xbm", "image/x-xbitmap");
            CONTENT_TYPE.Add("xpm", "image/x-xpixmap");
            CONTENT_TYPE.Add("xwd", "image/x-xwindowdump");
            CONTENT_TYPE.Add("igs", "model/iges");
            CONTENT_TYPE.Add("iges", "model/iges");
            CONTENT_TYPE.Add("msh", "model/mesh");
            CONTENT_TYPE.Add("mesh", "model/mesh");
            CONTENT_TYPE.Add("silo", "model/mesh");
            CONTENT_TYPE.Add("wrl", "model/vrml");
            CONTENT_TYPE.Add("vrml", "model/vrml");
            CONTENT_TYPE.Add("css", "text/css");
            CONTENT_TYPE.Add("html", "text/html");
            CONTENT_TYPE.Add("htm", "text/html");
            CONTENT_TYPE.Add("asc", "text/plain");
            CONTENT_TYPE.Add("txt", "text/plain");
            CONTENT_TYPE.Add("rtx", "text/richtext");
            CONTENT_TYPE.Add("rtf", "text/rtf");
            CONTENT_TYPE.Add("sgml", "text/sgml");
            CONTENT_TYPE.Add("sgm", "text/sgml");
            CONTENT_TYPE.Add("tsv", "text/tab-separated-values");
            CONTENT_TYPE.Add("wml", "text/vnd.wap.wml");
            CONTENT_TYPE.Add("wmls", "text/vnd.wap.wmlscript");
            CONTENT_TYPE.Add("etx", "text/x-setext");
            CONTENT_TYPE.Add("xsl", "text/xml");
            CONTENT_TYPE.Add("xml", "text/xml");
            CONTENT_TYPE.Add("mpeg", "video/mpeg");
            CONTENT_TYPE.Add("mpg", "video/mpeg");
            CONTENT_TYPE.Add("mpe", "video/mpeg");
            CONTENT_TYPE.Add("qt", "video/quicktime");
            CONTENT_TYPE.Add("mov", "video/quicktime");
            CONTENT_TYPE.Add("mxu", "video/vnd.mpegurl");
            CONTENT_TYPE.Add("avi", "video/x-msvideo");
            CONTENT_TYPE.Add("movie", "video/x-sgi-movie");
            CONTENT_TYPE.Add("wmv", "video/x-ms-wmv");
            CONTENT_TYPE.Add("asf", "video/x-ms-asf");
            CONTENT_TYPE.Add("ice", "x-conference/x-cooltalk");
        }

        /// <summary>
        /// Get ContentType
        /// </summary>
        /// <param name="type">Type</param>
        /// <returns>ContentType</returns>
        private static string GetContentType(string type)
        {
            // 获取文件对应的ContentType
            try
            {
                string contentType = CONTENT_TYPE[type];
                if (contentType != null)
                    return contentType;
            }
            catch { }
            return "application/octet-stream" + type;
        }

        /// <summary>
        /// Get ContentDisposition
        /// </summary>
        /// <param name="type">Type</param>
        /// <returns>ContentDisposition</returns>
        private static string GetDisposition(string type)
        {
            // 判断使用浏览器打开还是附件模式
            if (GetContentType(type).StartsWith("image"))
            {
                return "inline";
            }
            return "attachment";
        }

        /// <summary>
        /// Get File
        /// </summary>
        /// <param name="name">MD5 Name</param>
        /// <returns>File</returns>
        [HttpGet]
        [Route("{Id}")]
        public async Task<HttpResponseMessage> Get(string Id)
        {
            // Return 304
            // 判断是否含有Tag,有就返回浏览器缓存
            var tag = Request.Headers.IfNoneMatch.FirstOrDefault();
            if (Request.Headers.IfModifiedSince.HasValue && tag != null && tag.Tag.Length > 0)
                return new HttpResponseMessage(HttpStatusCode.NotModified);

            //查找该文件
            Resource model = await db.Resources.FindAsync(Id);

            //未找到文件
            if (model == null)
                return new HttpResponseMessage(HttpStatusCode.BadRequest);

            // 加载文件信息
            FileInfo info = new FileInfo(Path.Combine(ROOT_PATH, model.Folder, model.Id));
            // 文件没有找到
            if (!info.Exists)
                return new HttpResponseMessage(HttpStatusCode.BadRequest);

            FileStream file = null;
            try
            {
                // 打开文件
                file = new FileStream(info.FullName, FileMode.Open, FileAccess.Read, FileShare.Read);
                // 新建http响应
                HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
                // 设置头部信息
                // GetDisposition() 用于决定文件是直接打开还是提示下载
                // 一般来说图片文件就直接打开,其他文件就提示下载
                ContentDispositionHeaderValue disposition = new ContentDispositionHeaderValue(GetDisposition(model.Type));
                // 设置文件名+扩展
                disposition.FileName = string.Format("{0}.{1}", model.Name, model.Type);
                // 文件名
                disposition.Name = model.Name;
                // 文件大小
                disposition.Size = file.Length;

                if (file.Length < MEMORY_SIZE)
                {
                    // 如果 小于64MB 就直接拷贝到内存流中返回
                    // Copy To Memory And Close.
                    byte[] bytes = new byte[file.Length];
                    await file.ReadAsync(bytes, 0, (int)file.Length);
                    file.Close();
                    MemoryStream ms = new MemoryStream(bytes);

                    result.Content = new ByteArrayContent(ms.ToArray());
                }
                else
                {
                    // 如果不是就直接打包为文件流返回
                    result.Content = new StreamContent(file);
                }

                // 设置文件在网络中的ContentType
                // GetContentType() 方法是根据扩展名去查找字典
                // 字典中我收集了大概150种,基本足够使用了
                result.Content.Headers.ContentType = new MediaTypeHeaderValue(GetContentType(model.Type));
                result.Content.Headers.ContentDisposition = disposition;

                // 设置浏览器缓存相关
                // 设置缓存Expires
                result.Content.Headers.Expires = new DateTimeOffset(DateTime.Now).AddHours(1);
                // 缓存最后修改时间
                result.Content.Headers.LastModified = new DateTimeOffset(model.Published);
                // 缓存控制
                result.Headers.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromHours(1) };
                // 设置ETag,这里的ETag为了方便就直接使用MD5值
                result.Headers.ETag = new EntityTagHeaderValue(string.Format("\"{0}\"", model.Id));

                // 返回请求
                return result;
            }
            catch
            {
                if (file != null)
                {
                    file.Close();
                }
            }

            return new HttpResponseMessage(HttpStatusCode.BadRequest);
        }


        /// <summary>
        /// Post File
        /// </summary>
        /// <param name="Id">Md5</param>
        /// <returns>Resource</returns>
        [HttpPost]
        [Route("Upload/{Id?}")]
        [ResponseType(typeof(Resource))]
        public async Task<IHttpActionResult> Post(string Id = null)
        {
            List<Resource> resources = new List<Resource>();

            // multipart/form-data
            var provider = new MultipartMemoryStreamProvider();
            await Request.Content.ReadAsMultipartAsync(provider);

            foreach (var item in provider.Contents)
            {
                if (item.Headers.ContentDisposition.FileName != null)
                {
                    //Strem
                    var ms = item.ReadAsStreamAsync().Result;

                    using (var br = new BinaryReader(ms))
                    {
                        if (ms.Length <= 0)
                            break;
                        var data = br.ReadBytes((int)ms.Length);
                        //Md5
                        string id = HashUtils.GetMD5Hash(data);

                        Resource temp = await db.Resources.FindAsync(id);
                        if (temp == null)
                        {
                            //Create
                            Resource resource = new Resource();
                            resource.Id = id;
                            resource.Size = ms.Length;
                            resource.Cursor = resource.Size;
                            resource.Published = DateTime.Now;
                            //Info
                            FileInfo info = new FileInfo(item.Headers.ContentDisposition.FileName.Replace("\"", ""));
                            resource.Type = info.Extension.Substring(1).ToLower();
                            resource.Name = info.Name.Substring(0, info.Name.LastIndexOf("."));
                            //Relative
                            resource.Folder = DateTime.Now.ToString("yyyyMM/dd/", DateTimeFormatInfo.InvariantInfo);

                            //Write
                            try
                            {
                                string dirPath = Path.Combine(ROOT_PATH, resource.Folder);
                                if (!Directory.Exists(dirPath))
                                {
                                    Directory.CreateDirectory(dirPath);
                                }

                                File.WriteAllBytes(Path.Combine(dirPath, resource.Id), data);
                                //Save To Datebase
                                db.Resources.Add(resource);
                                await db.SaveChangesAsync();
                                temp = await db.Resources.FindAsync(resource.Id);
                            }
                            catch { }
                        }
                        if (temp != null)
                            resources.Add(temp);
                    }
                }
            }

            if (resources.Count == 0)
                return BadRequest();
            else if (resources.Count == 1)
                return Ok(resources.FirstOrDefault());
            else
                return Ok(resources);
        }
    }
}
这个是修改后的 API文件, Post相对上一章没有进行修改;添加了 Get,和其他几个方法以及变量。

RunTime

Api


运行之后首先来看看现在的API,一个是上传,一个是访问;简单吧?

Home/Upload


上传两个,一个图片一个Zip


可以看见返回了两个文件的信息,分别记住两个文件的ID,来访问一下试试。

Image


可以看见图片是直接打开在浏览器中的;下面来看看Zip的文件咋样。


可以看见是以下载文件的方式返回的。

请求返回
Zip


通过浏览器调试,我们可以看见Zip文件的返回信息与上面Get方法中设置的参数是一样的;代表了方法正确执行。

Image


可以看见图片的是以 Inline 的形式打开的;所以是在浏览器中直接显示。

再次请求图片:


通过再次请求图片;可以看见服务器返回的是304.意思就是浏览器自己使用自己的缓存信息;无需重复从服务器加载。这个无疑试用与图片的情况。

所有的工作都是按照我们预想的进行;能得到这样的结果是不错的。


以后你的网站中就能使用:.../Res/.....简单的一个地址就OK了;无论是图片;附件;css;js;等等都是可行的。而且也没有暴露你的文件存储信息

你敢说不爽?反正我是觉得挺爽的!


END

有到了结束的时候了。

资源
下一章

针对于下一章,有两个方向:

  • 一个是开发一个管理界面
  • 还有就是开发针对图片的Get方法(这个方法能实现图片的压缩,分割等功能)

但是没有想好,大伙想看关于那个的?

========================================================
作者:
qiujuer
博客:blog.csdn.net/qiujuer
网站:www.qiujuer.net
开源库:Genius-Android
转载请注明出处:http://blog.csdn.net/qiujuer/article/details/41744733
========================================================


  • 4
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值