WebApi使用 (.Net Framework版)

打印 上一主题 下一主题

主题 879|帖子 879|积分 2637

1 创建

使用.Net做web后端,推荐使用.Net Core,微软在此底子上做了很多适配,包括内置Swagger,可以直接启动等等。而.Net Framework版,需要手动配置很多内容。
如果需要调用的项目是基于.Net Framework,那么web项目也应基于.Net Framework开发。大概其他原因不得不使用.Net Framework开发web项目,可以参考本文。
打开VS,在搜索栏输入“ASP.NET Web”,选择.Net Framework版,留意,这里要创建的是空白API项目,在前后端分离的项目中只作为后端接口,而不是MVC(模型-视图-控制器)

填写项目名,选择位置,选择需要的框架

创建一个空白项目,勾选“Web API”,取消勾选“HTTPS配置”

添加完成后,会创建以下目录布局:

  • “App_Data”目录用于方式一些资源文件;
  • “App_Start”目录下用于放置一些配置资源,默认已有一个配置类“WebApiConfig”,内容如下:
  1. public static class WebApiConfig
  2. {
  3.     public static void Register(HttpConfiguration config)
  4.     {
  5.         config.MapHttpAttributeRoutes();
  6.         config.Routes.MapHttpRoute(
  7.             name: "DefaultApi",
  8.             routeTemplate: "api/{controller}/{id}",
  9.             defaults: new { id = RouteParameter.Optional }
  10.         );
  11.     }
  12. }
复制代码
  1. - config.MapHttpAttributeRoutes();表示启用了属性路由,允许在控制器和操作方法上直接使用特性(如 `[Route]`)来定义路由规则。
  2. - 下半部分表示定义默认路由规则。
复制代码

  • “Controllers”目录下放置对外接口;
  • “Models”目录下放置后端接口的内部逻辑,比如要接入数据库的操纵等等。这些目录作为一个规范,如果违反这个规范随意放置,也可以正常运行,只不过看着比较杂乱。

2 测试

项目创建完成后,并没有提供任何对外接口,添加一个测试接口。选中“Controllers”目录=>右键=>添加=>Web API控制器类

填写名称,建议名称为“xxxController”(xxx为需要写的名称),此名称会被上文提到的“默认路由规则”匹配,将“xxx”作为api的一部门。

类创建完成后,会自动生成示例步伐,包含Get, Post, Put, Delete请求
  1. public class TestController : ApiController
  2. {
  3.     // GET api/<controller>
  4.     public IEnumerable<string> Get()
  5.     {
  6.         return new string[] { "value1", "value2" };
  7.     }
  8.     // GET api/<controller>/5
  9.     public string Get(int id)
  10.     {
  11.         return "value";
  12.     }
  13.     // POST api/<controller>
  14.     public void Post([FromBody] string value)
  15.     {
  16.     }
  17.     // PUT api/<controller>/5
  18.     public void Put(int id, [FromBody] string value)
  19.     {
  20.     }
  21.     // DELETE api/<controller>/5
  22.     public void Delete(int id)
  23.     {
  24.     }
  25. }
复制代码
启动项目,基于.Net Framework的web项目需要借助于服务启动,vs调试默认使用IIS服务。

启动完成后,没有配置默认的访问地址就会表现如下界面。

测试Api访问。

3 配置属性路由规则

上文提到,配置类“WebApiConfig”中配置了默认路由规则,启用了属性路由。属性路由就是使用特性标志路由,使用属性路由取代了默认路由。
示例如下:
  1. [RoutePrefix("api/TestABC")]
  2. public class TestController : ApiController
  3. {
  4.     [HttpGet]
  5.     [Route("GetValue")]
  6.     // GET api/<controller>
  7.     public IEnumerable<string> Get()
  8.     {
  9.         return new string[] { "value1", "value2" };
  10.     }
  11. }
复制代码
启动,测试访问,可以看到,路由规则已经由默认路由规则变为了属性路由规则。

4 配置Swagger

4.1 基本配置

在NuGet中下载安装“Swashbuckle”

安装完成后,在“App_Start”目录下会自动生成“SwaggerConfig”配置类。可以修改需要表现的内容,下图这个语句包含两条信息,“版本”和“Title”。

在项目属性中,勾选生成“XML文档文件”,本质上是Swagger将此xml转换为Swagger格式的内容。

安装完之后,再访问本地URL,可能会报一个错,这里不要慌,一般是安装的“Swashbuckle”包依赖的内容与现有安装的包不符合,在NuGet包管理器中全部更到最新即可。



启动后在原有url后参加/swagger即可访问文档。

展开后,可以点击“Try it out”按钮进行测试。

请求与相应如下:

测试Post,字符串一定要带""

步伐中获取到欣赏器发送的内容。

4.2 为步伐添加表明

上文提到,勾选了生成“XML文档文件”,此xml是将写的表明记录下来。
比如为Get请求添加表明。
  1. /// <summary>
  2. /// 测试请求
  3. /// </summary>
  4. /// <returns>返回示例数据</returns>
  5. public IEnumerable<string> Get()
  6. {
  7.     return new string[] { "value1", "value2" };
  8. }
复制代码
然后在SwaggerConfig配置类中添加一条配置。
  1. c.IncludeXmlComments(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "bin\\TestWebApi.xml"));
复制代码

再次访问,可以看到,有了表明的内容。

4.3 汉化处置惩罚

安装“Swagger.Net.UI”

安装完成后,在“App_Start”目录下新增了一个SwaggerNet类

打开SwaggerNet类,表明掉这两行(这里我没做深究,参考的几篇文章都说表明掉这两行,可能后续运行有报错)

创建一个“SwaggerControllerDescProvider”类,用于对swagger文档中的内容进行汉化处置惩罚。
  1. /// <summary>
  2. /// swagger显示控制器的描述
  3. /// </summary>
  4. public class SwaggerControllerDescProvider : ISwaggerProvider
  5. {
  6.     private readonly ISwaggerProvider _swaggerProvider;
  7.     private static ConcurrentDictionary<string, SwaggerDocument> _cache = new ConcurrentDictionary<string, SwaggerDocument>();
  8.     private readonly string _xml;
  9.     /// <summary>
  10.     ///
  11.     /// </summary>
  12.     /// <param name="swaggerProvider"></param>
  13.     /// <param name="xml">xml文档路径</param>
  14.     public SwaggerControllerDescProvider(ISwaggerProvider swaggerProvider, string xml)
  15.     {
  16.         _swaggerProvider = swaggerProvider;
  17.         _xml = xml;
  18.     }
  19.     public SwaggerDocument GetSwagger(string rootUrl, string apiVersion)
  20.     {
  21.         var cacheKey = string.Format("{0}_{1}", rootUrl, apiVersion);
  22.         SwaggerDocument srcDoc = null;
  23.         //只读取一次
  24.         if (!_cache.TryGetValue(cacheKey, out srcDoc))
  25.         {
  26.             srcDoc = _swaggerProvider.GetSwagger(rootUrl, apiVersion);
  27.             srcDoc.vendorExtensions = new Dictionary<string, object> { { "ControllerDesc", GetControllerDesc() } };
  28.             _cache.TryAdd(cacheKey, srcDoc);
  29.         }
  30.         return srcDoc;
  31.     }
  32.     /// <summary>
  33.     /// 从API文档中读取控制器描述
  34.     /// </summary>
  35.     /// <returns>所有控制器描述</returns>
  36.     public ConcurrentDictionary<string, string> GetControllerDesc()
  37.     {
  38.         string xmlpath = _xml;
  39.         ConcurrentDictionary<string, string> controllerDescDict = new ConcurrentDictionary<string, string>();
  40.         if (File.Exists(xmlpath))
  41.         {
  42.             XmlDocument xmldoc = new XmlDocument();
  43.             xmldoc.Load(xmlpath);
  44.             string type = string.Empty, path = string.Empty, controllerName = string.Empty;
  45.             string[] arrPath;
  46.             int length = -1, cCount = "Controller".Length;
  47.             XmlNode summaryNode = null;
  48.             foreach (XmlNode node in xmldoc.SelectNodes("//member"))
  49.             {
  50.                 type = node.Attributes["name"].Value;
  51.                 if (type.StartsWith("T:"))
  52.                 {
  53.                     //控制器
  54.                     arrPath = type.Split('.');
  55.                     length = arrPath.Length;
  56.                     controllerName = arrPath[length - 1];
  57.                     if (controllerName.EndsWith("Controller"))
  58.                     {
  59.                         //获取控制器注释
  60.                         summaryNode = node.SelectSingleNode("summary");
  61.                         string key = controllerName.Remove(controllerName.Length - cCount, cCount);
  62.                         if (summaryNode != null && !string.IsNullOrEmpty(summaryNode.InnerText) && !controllerDescDict.ContainsKey(key))
  63.                         {
  64.                             controllerDescDict.TryAdd(key, summaryNode.InnerText.Trim());
  65.                         }
  66.                     }
  67.                 }
  68.             }
  69.         }
  70.         return controllerDescDict;
  71.     }
  72. }
复制代码
在SwaggerUI文件夹中,创建一个swagger_lang.js的js,用于对swagger进行汉化处置惩罚(注:这个文件必须添加,否则汉化将失败)
swagger_lang.js 文件中的js内容如下。在末了几行有“公司名称”和对应的Url,可以自行更改,这里我写了百度的链接作为测试。
  1. /// <summary>
  2. /// 中文转换
  3. /// </summary>
  4. var SwaggerTranslator = (function () {
  5.     //定时执行检测是否转换成中文,最多执行500次  即500*50/1000=25s
  6.     var iexcute = 0,
  7.         //中文语言包
  8.         _words = {
  9.             "Warning: Deprecated": "警告:已过时",
  10.             "Implementation Notes": "实现备注",
  11.             "Response Class": "响应类",
  12.             "Status": "状态",
  13.             "Parameters": "参数",
  14.             "Parameter": "参数",
  15.             "Value": "值",
  16.             "Description": "描述",
  17.             "Parameter Type": "参数类型",
  18.             "Data Type": "数据类型",
  19.             "Response Messages": "响应消息",
  20.             "HTTP Status Code": "HTTP状态码",
  21.             "Reason": "原因",
  22.             "Response Model": "响应模型",
  23.             "Request URL": "请求URL",
  24.             "Response Body": "响应体",
  25.             "Response Code": "响应码",
  26.             "Response Headers": "响应头",
  27.             "Hide Response": "隐藏响应",
  28.             "Headers": "头",
  29.             "Try it out!": "试一下!",
  30.             "Show/Hide": "显示/隐藏",
  31.             "List Operations": "显示操作",
  32.             "Expand Operations": "展开操作",
  33.             "Raw": "原始",
  34.             "can't parse JSON.  Raw result": "无法解析JSON. 原始结果",
  35.             "Model Schema": "模型架构",
  36.             "Model": "模型",
  37.             "apply": "应用",
  38.             "Username": "用户名",
  39.             "Password": "密码",
  40.             "Terms of service": "服务条款",
  41.             "Created by": "创建者",
  42.             "See more at": "查看更多:",
  43.             "Contact the developer": "联系开发者",
  44.             "api version": "api版本",
  45.             "Response Content Type": "响应Content Type",
  46.             "fetching resource": "正在获取资源",
  47.             "fetching resource list": "正在获取资源列表",
  48.             "Explore": "浏览",
  49.             "Show Swagger Petstore Example Apis": "显示 Swagger Petstore 示例 Apis",
  50.             "Can't read from server.  It may not have the appropriate access-control-origin settings.": "无法从服务器读取。可能没有正确设置access-control-origin。",
  51.             "Please specify the protocol for": "请指定协议:",
  52.             "Can't read swagger JSON from": "无法读取swagger JSON于",
  53.             "Finished Loading Resource Information. Rendering Swagger UI": "已加载资源信息。正在渲染Swagger UI",
  54.             "Unable to read api": "无法读取api",
  55.             "from path": "从路径",
  56.             "Click to set as parameter value": "点击设置参数",
  57.             "server returned": "服务器返回"
  58.         },
  59.         //定时执行转换
  60.         _translator2Cn = function () {
  61.             if ($("#resources_container .resource").length > 0) {
  62.                 _tryTranslate();
  63.             }
  64.             if ($("#explore").text() == "Explore" && iexcute < 500) {
  65.                 iexcute++;
  66.                 setTimeout(_translator2Cn, 50);
  67.             }
  68.         },
  69.         //设置控制器注释
  70.         _setControllerSummary = function () {
  71.             $.ajax({
  72.                 type: "get",
  73.                 async: true,
  74.                 url: $("#input_baseUrl").val(),
  75.                 dataType: "json",
  76.                 success: function (data) {
  77.                     var summaryDict = data.ControllerDesc;
  78.                     var id, controllerName, strSummary;
  79.                     $("#resources_container .resource").each(function (i, item) {
  80.                         id = $(item).attr("id");
  81.                         if (id) {
  82.                             controllerName = id.substring(9);
  83.                             strSummary = summaryDict[controllerName];
  84.                             if (strSummary) {
  85.                                 var option = $(item).children(".heading").children(".options");
  86.                                 if ($(option).children(".controller-summary").length > 0) {
  87.                                     $(option).children(".controller-summary").remove();
  88.                                 }
  89.                                 $(option).prepend('<li  title="' + strSummary + '">' + strSummary + '</li>');
  90.                             }
  91.                         }
  92.                     });
  93.                 }
  94.             });
  95.         },
  96.         //尝试将英文转换成中文
  97.         _tryTranslate = function () {
  98.             $('[data-sw-translate]').each(function () {
  99.                 $(this).html(_getLangDesc($(this).html()));
  100.                 $(this).val(_getLangDesc($(this).val()));
  101.                 $(this).attr('title', _getLangDesc($(this).attr('title')));
  102.             });
  103.         },
  104.         _getLangDesc = function (word) {
  105.             return _words[$.trim(word)] !== undefined ? _words[$.trim(word)] : word;
  106.         };
  107.     return {
  108.         Translator: function () {
  109.             $("#logo").html("公司名称").attr("href", "https://www.baidu.com");
  110.             $('body').append('');
  111.             //设置控制器描述
  112.             _setControllerSummary();
  113.             _translator2Cn();
  114.         },
  115.         translate: function () {
  116.             this.Translator();
  117.         }
  118.     }
  119. })();
  120. //执行转换
  121. SwaggerTranslator.Translator();
复制代码
新增的swagger_lang.js文件需要修改文件属性,将文件生成操纵修改为“嵌入的资源”。

将创建的swagger_lang.js在SwaggerConfig文件中进行引用。留意文件的路径。
  1. c.InjectJavaScript(thisAssembly, "TestWebApi.SwaggerUI.swagger_lang.js");  //引用中文包
复制代码

汉化结果:

5 包装返回结果

5.1 IHttpActionResult接口

默认生成的测试步伐,请求到的数据都直接返回的数据内容。
  1. public IEnumerable<string> Get()
  2. {
  3.     return new string[] { "value1", "value2" };
  4. }
  5. // GET api/<controller>/5
  6. public string Get(int id)
  7. {
  8.     return "value";
  9. }
复制代码
如果需要自主决定状态码大概返回错误结果内容时携带异常信息等,可以使用C#提供的IHttpActionResult接口。
  1. public IHttpActionResult Get()
  2. {
  3.     return Ok(new string[] { "value1", "value2" });
  4. }
复制代码
其中Ok()是ASP.NET Web API 提供了一些常用的IHttpActionResult实现,代表200状态码,只使用Ok(),与不使用IHttpActionResult接口并无显着的区别。

5.2 其它方法

除了常见的 Ok 方法外,ASP.NET Web API 还提供了多个 IHttpActionResult 的实现类或方法,用于构建特定的 HTTP 相应。以下是常用的 IHttpActionResult 实现及其对应的用途:
5.2.1 Ok


  • 返回 HTTP 状态码:200 OK
  • 用法

    • 用于返回一个成功的相应,并可以附带一个对象作为相应体。

  1. public IHttpActionResult Get()
  2. {
  3.     var data = new { Id = 1, Name = "Example" };
  4.     return Ok(data);
  5. }
复制代码
5.2.2 BadRequest


  • 返回 HTTP 状态码:400 Bad Request
  • 用法

    • 用于表示客户端发送的请求无效,例如参数验证失败或格式错误。

  1. public IHttpActionResult Post(string value)
  2. {
  3.     if (string.IsNullOrEmpty(value))
  4.     {
  5.         return BadRequest("Value cannot be null or empty.");
  6.     }
  7.     return Ok();
  8. }
复制代码
5.2.3 Unauthorized


  • 返回 HTTP 状态码:401 Unauthorized
  • 用法

    • 用于指示需要身份验证或身份验证失败。

  1. public IHttpActionResult Get()
  2. {
  3.     if (!User.Identity.IsAuthenticated)
  4.     {
  5.         return Unauthorized();
  6.     }
  7.     return Ok();
  8. }
复制代码
5.2.4 NotFound


  • 返回 HTTP 状态码:404 Not Found
  • 用法

    • 用于表示所请求的资源不存在。

  1. public IHttpActionResult Get(int id)
  2. {
  3.     var item = _repository.GetItem(id);
  4.     if (item == null)
  5.     {
  6.         return NotFound();
  7.     }
  8.     return Ok(item);
  9. }
复制代码
5.2.5 Conflict


  • 返回 HTTP 状态码:409 Conflict
  • 用法

    • 用于表示请求无法完成,因为发生了冲突(例如重复数据或违反束缚)。

  1. public IHttpActionResult Post(Item item)
  2. {
  3.     if (_repository.Contains(item.Id))
  4.     {
  5.         return Conflict();
  6.     }
  7.     _repository.Add(item);
  8.     return Ok();
  9. }
复制代码
5.2.6 Created / CreatedAtRoute


  • 返回 HTTP 状态码:201 Created
  • 用法

    • 用于表示资源已成功创建,并可以附带新资源的 URI。

  1. public IHttpActionResult Post(Item item)
  2. {
  3.     _repository.Add(item);
  4.     return Created(new Uri(Request.RequestUri + "/" + item.Id), item);
  5. }
  6. // 或者使用 CreatedAtRoute 指定路由名称
  7. public IHttpActionResult Post(Item item)
  8. {
  9.     _repository.Add(item);
  10.     return CreatedAtRoute("DefaultApi", new { id = item.Id }, item);
  11. }
复制代码
5.2.7 NoContent


  • 返回 HTTP 状态码:204 No Content
  • 用法

    • 用于表示请求已成功处置惩罚,但不需要返回相应体内容(例如更新操纵)。

  1. public IHttpActionResult Put(int id, Item item)
  2. {
  3.     if (!_repository.Update(id, item))
  4.     {
  5.         return NotFound();
  6.     }
  7.     return StatusCode(HttpStatusCode.NoContent); // 或者直接 NoContent()
  8. }
复制代码
5.2.8 Redirect / RedirectToRoute


  • 返回 HTTP 状态码:302 Found 或其他重定向状态码
  • 用法

    • 用于表示需要重定向到另一个 URI。

  1. public IHttpActionResult Get()
  2. {
  3.     return Redirect("https://www.example.com");
  4. }
  5. // 使用特定路由
  6. public IHttpActionResult Get()
  7. {
  8.     return RedirectToRoute("DefaultApi", new { id = 1 });
  9. }
复制代码
5.2.9 InternalServerError


  • 返回 HTTP 状态码:500 Internal Server Error
  • 用法

    • 用于表示服务器内部发生错误。

  1. public IHttpActionResult Get()
  2. {
  3.     try
  4.     {
  5.         // 处理逻辑
  6.     }
  7.     catch (Exception ex)
  8.     {
  9.         return InternalServerError(ex);
  10.     }
  11. }
复制代码
5.2.10 ResponseMessage


  • 返回自界说的 HTTP 相应消息
  • 用法

    • 用于完全自界说 HTTP 相应,灵活构建相应消息。

  1. public IHttpActionResult Get()
  2. {
  3.     var response = Request.CreateResponse(HttpStatusCode.OK, "Custom Message");
  4.     return ResponseMessage(response);
  5. }
复制代码
5.2.11 StatusCode


  • 返回恣意 HTTP 状态码
  • 用法

    • 用于返回特定的 HTTP 状态码,而不需要返回额外的内容。

  1. public IHttpActionResult Delete(int id)
  2. {
  3.     _repository.Remove(id);
  4.     return StatusCode(HttpStatusCode.NoContent); // 返回 204 No Content
  5. }
复制代码
5.2.12 Json


  • 返回 JSON 格式的相应
  • 用法

    • 用于直接返回 JSON 数据,而不依赖模型绑定。

  1. public IHttpActionResult Get()
  2. {
  3.     var data = new { Id = 1, Name = "Example" };
  4.     return Json(data);
  5. }
复制代码
5.2.13 ExceptionResult


  • 返回异常结果
  • 用法

    • 用于返回一个表示异常的相应。

  1. public IHttpActionResult Get()
  2. {
  3.     try
  4.     {
  5.         throw new InvalidOperationException("An unexpected error occurred.");
  6.     }
  7.     catch (Exception ex)
  8.     {
  9.         return InternalServerError(ex); // 适用于返回异常信息的场景
  10.     }
  11. }
复制代码
5.2.14 NotImplemented


  • 返回 HTTP 状态码:501 Not Implemented
  • 用法

    • 用于表示服务端还未实现某个功能。

  1. public IHttpActionResult Get()
  2. {
  3.     return StatusCode(HttpStatusCode.NotImplemented);
  4. }
复制代码
5.2.15 总结

以下是常见的 IHttpActionResult 方法及其对应的 HTTP 状态码:
方法HTTP 状态码形貌Ok200 OK请求成功并返回数据。BadRequest400 Bad Request客户端请求无效。Unauthorized401 Unauthorized表示未通过身份验证。NotFound404 Not Found资源不存在。Conflict409 Conflict请求冲突(如违反束缚)。Created201 Created成功创建资源,并返回资源 URI。NoContent204 No Content请求成功,但没有返回内容。Redirect302 Found重定向到另一个 URI。InternalServerError500 Internal Server Error服务器内部错误。ResponseMessage自界说状态码返回自界说的 HTTP 相应消息。StatusCode恣意状态码返回指定的 HTTP 状态码。Json恣意状态码返回 JSON 相应内容。通过这些方法,开发者可以灵活地编写符合 HTTP 规范的相应,满足各种 RESTful API 的需求。
6 标明返回类型

书接第五部门,返回类型如果使用IHttpActionResult接口,Swagger文档中无法解析出相应的详细内容,如下图所示。

如果希望表现相应内容,可以使用特性标明。
  1. [SwaggerResponse(200, "Success", typeof(string[]))]public IHttpActionResult Get()
  2. {
  3.     return Ok(new string[] { "value1", "value2" });
  4. }
复制代码

有关这方面的扩展内容很多,感兴趣可以自行查阅。
7 一键启动

在项目的Web.config文件同目录下创建一个.bat文件

.bat文件的内容如下,相关信息修改为适合本项目的内容。这里有一点,正常来说应该先启动IIS Express,再执行“欣赏器访问URL”,但是启动IIS Express这个行为会壅闭到当前语句,不再往下执行,所以这里将“欣赏器访问URL”语句提前,欣赏器会等候IIS Express启动后访问成功。
  1. @echo off
  2. :: 设置IIS Express的路径(通常位于Program Files中,如果安装在其他位置,请调整路径)
  3. set IIS_EXPRESS_PATH="C:\Program Files\IIS Express\iisexpress.exe"
  4. :: 项目的物理路径,这条语句不需要修改,意为访问bat文件所在目录
  5. cd /d "%~dp0"
  6. :: Web项目的端口
  7. set PORT=63027
  8. :: 项目的启动URL(如果需要)
  9. set URL=http://localhost:%PORT%/swagger/ui/index#/
  10. :: 打开默认浏览器访问URL(可选)
  11. start %URL%
  12. :: 启动IIS Express
  13. %IIS_EXPRESS_PATH% /path:"%cd%" /port:%PORT%
  14. pause
复制代码
双击该bat文件即可将项目内容自动配置到IIS服务中。
8 打包

选择“发布”。

发布位置选择“文件夹”。

点击“发布”。

生成的内容如下:

将“配置Swagger”中生成的xml文件拷到bin目录下,这是Swagger文档的底子,发布操纵不会自动复制该文件,需要手动复制。然后将“一键启动”生成的.bat文件复制到Web.config文件同目录下,双击即可打开。

9 参考

.net framework中webapi使用swagger进行接口文档展示
ASP.NET WebApi项目框架搭建(一):创建项目
WebApi 接口返回值类型详解 ( 转 )
如果以为文章还不错的话,请给一个大大的赞吧,感谢支持!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

莱莱

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表