筆記-ASP.NET WebApi


本文是針對ASP.NET WepApi 2 的筆記。

Web API 可返回的結果:

1.void

2.HttpResponseMessage

3.IHttpActionResult

4.其他類型

返回類型 Web API創建響應的方式
void HTTP204(無內容)
HttpResponseMessage 轉換為HTTP響應消息
IHttpActionRsult 調用ExecuteAsync來創建HttpResponseMessage,然后轉換為HTTP響應消息
其他類型 將序列化的返回值寫到響應正文中,返回HTTP200

 

路由

路由表:Web API的默認路由模板"api/{controller}/{id}",此模板中,"api"是文本路徑段,{controller}和{id}是占位符變量。

路由變體:可以顯示指定Action的HTTP方法,如HttpGet,HttpPost。

public class ProductsController : ApiController
{
    [HttpGet]
    public Product FindProduct(id) {}
}

若要允許多個HTTP方法或GET,POST,PUT,DELETE以外的HTTP方法,可以顯示使用AcceptVerbs屬性:

public class ProductsController : ApiController
{
    [AcceptVerbs("GET", "HEAD")]
    public Product FindProduct(id) { }

    // WebDAV method
    [AcceptVerbs("MKCOL")]
    public void MakeCollection() { }
}

可以通過ActionName屬性重寫Action名稱:

public class ProductsController : ApiController
{
    [HttpGet]
    [ActionName("Thumbnail")]
    public HttpResponseMessage GetThumbnailImage(int id);

    [HttpPost]
    [ActionName("Thumbnail")]
    public void AddThumbnailImage(int id);
}

也可以使用NonAction屬性指定Action不參與路由匹配:

// Not an action method.
[NonAction]  
public string GetPrivateData() { ... }

Web API路由過程的某些部分提供了擴展點:

接口 描述
IHttpControllerSelector 選擇控制器
IHttpControllerTypeResolver 獲取控制器類型的列表。DefaultHttpControllerSelector從此列表中選擇控制器類型
IAssembliesResolver 獲取項目的程序集的列表。IHttpControllerTypeResolver接口使用此列表找到控制器的類型
IHttpControllerActivator 創建新的控制器實例
IHttpActionSelector 選擇Action
IHttpActionInvoker 調用Action

若要為這些接口提供自己的實現,在HttpConfiguration對象上操作,HttpConfigutation在App_Start文件夾下的WebApiConfig.cs文件下能找到,也可以自己獲取:

var config = GlobalConfiguration.Configuration;
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));

屬性路由:Route是API 2支持的一種新類型的路由,顧名思義,它可以使用屬性定義路由:

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

你可能需要先啟用,才能使用屬性路由:

using System.Web.Http;

namespace WebApplication
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();

            // Other Web API configuration not shown.
        }
    }
}

路由名稱:

在Web API 中,每個路由都有一個名稱。路由名稱可用於生成鏈接,以便可以在HTTP響應中包含一個鏈接。

Route可以設置名稱屬性,下面的代碼演示如何設置路由名稱,以及如何使用根據路由名稱生成的鏈接:

public class BooksController : ApiController
{
    [Route("api/books/{id}", Name="GetBookById")]
    public BookDto GetBook(int id) 
    {
        // Implementation not shown...
    }

    [Route("api/books")]
    public HttpResponseMessage Post(Book book)
    {
        // Validate and add book to database (not shown)

        var response = Request.CreateResponse(HttpStatusCode.Created);

        // Generate a link to the new book and set the Location header in the response.
        string uri = Url.Link("GetBookById", new { id = book.BookId });
        response.Headers.Location = new Uri(uri);
        return response;
    }
}

路由順序:屬性路由還可以指定路由匹配時的順序,RouteOrder,默認順序是0,首先計算較低的值。

媒體格式化

媒體類型,也稱MIME類型。在HTTP中,媒體類型描述消息正文的格式。例如:

  • text/html
  • image/png
  • application/json

當HTTP消息包含實體正文時,Content-type標頭指定消息正文的格式。

例如,如果HTTP響應包含PNG圖像,響應可能包含以下標頭:

HTTP/1.1 200 OK
Content-Length: 95267
Content-Type: image/png

當客戶端發送請求時,它可以包含Accept標頭。Accept標頭指示客服端想要從服務器獲取的響應媒體類型。例如:

Accept: text/html,application/xhtml+xml,application/xml

此標頭指示客戶端希望獲得HTML、XHTML或XML。

媒體類型可以確定Web API 如何序列化和反序列化HTTP消息正文。可以通過編寫自己的媒體類型達到擴展Web API序列化和反序列化的目的。

若要創建媒體格式化程序,需要實現以下類:

  • MediaTypeFormatter。 此類使用異步的方式讀取和寫入。
  • BufferedMediaTypeFormatter。此類派生自MediaTypeFormatter,但是使用同步的方式讀取和寫入。

下面的實例演示的媒體類型可以序列化以逗號分割值(CSV)格式的產品對象。下面是Product對象的定義:

namespace ProductStore.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

若要實現CSV格式化程序,定義一個類,派生自BufferedMediaTypeFormatter:

namespace ProductStore.Formatters
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using ProductStore.Models;

namespace ProductStore.Formatters
{
    public class ProductCsvFormatter : BufferedMediaTypeFormatter
    {
    }
}

在構造函數中,添加格式化程序支持的媒體類型。在此示例中,支持類型"text/csv":

public ProductCsvFormatter()
{
    // Add the supported media type.
    SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
}

重寫CanWriteType方法,以指示該類型格式化程序可以序列化:

public override bool CanWriteType(System.Type type)
{
    if (type == typeof(Product))
    {
        return true;
    }
    else
    {
        Type enumerableType = typeof(IEnumerable<Product>);
        return enumerableType.IsAssignableFrom(type);
    }
}

在此示例中,格式化程序可以序列化單個Product對象的集合、Product對象。

同樣,重寫CanReadType方法,以指示該類型格式化程序可以反序列化。在此示例中,格式化程序不支持反序列化,因此,此方法返回false:

public override bool CanReadType(Type type)
{
    return false;
}

最后,重寫WriteToStream方法。此方法通過向流寫入序列化程序。如果支持反序列化,還可以重寫ReadFromStream方法。

public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
{
    using (var writer = new StreamWriter(writeStream))
    {
        var products = value as IEnumerable<Product>;
        if (products != null)
        {
            foreach (var product in products)
            {
                WriteItem(product, writer);
            }
        }
        else
        {
            var singleProduct = value as Product;
            if (singleProduct == null)
            {
                throw new InvalidOperationException("Cannot serialize type");
            }
            WriteItem(singleProduct, writer);
        }
    }
}

// Helper methods for serializing Products to CSV format. 
private void WriteItem(Product product, StreamWriter writer)
{
    writer.WriteLine("{0},{1},{2},{3}", Escape(product.Id),
        Escape(product.Name), Escape(product.Category), Escape(product.Price));
}

static char[] _specialChars = new char[] { ',', '\n', '\r', '"' };

private string Escape(object o)
{
    if (o == null)
    {
        return "";
    }
    string field = o.ToString();
    if (field.IndexOfAny(_specialChars) != -1)
    {
        // Delimit the entire field with quotes and replace embedded quotes with "".
        return String.Format("\"{0}\"", field.Replace("\"", "\"\""));
    }
    else return field;
}

將自定義的媒體格式化程序添加到Web API管道

public static void ConfigureApis(HttpConfiguration config)
{
    config.Formatters.Add(new ProductCsvFormatter()); 
}

WebAPI 提供了對JSON和XML的媒體類型格式化程序。

JSON格式由JsonMediaTypeFormatter類處理,默認情況下JsonMediaTypeFormatter使用Json.NET庫以執行序列化。

如果您願意,可以配置JsonMediaTypeFormatter,以使用DataContractJsonSerializer而不是Json.NET。若要執行此操作,設置UseDataContractJsonSerializer屬性true:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.UseDataContractJsonSerializer = true;

默認情況下,所有公共屬性和字段會參與序列化JSON。若要忽略屬性或字段,給它裝飾JsonIgnore屬性。

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    [JsonIgnore]
    public int ProductCode { get; set; } // omitted
}

你也可以使用DataContract屬性裝飾你的類,存在此屬性,則將忽略所有成員,除非它們具有DataMember(JsonProperty)屬性。此外,可以使用DataMember序列化私有成員。

[DataContract]
public class Product
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public decimal Price { get; set; }
    public int ProductCode { get; set; }  // omitted by default
}

默認情況下,只讀屬性參與序列化。

默認情況下,Json.NET將日期將采用ISO 8601格式。使用"Z"后綴編寫日期以 UTC (協調世界時)。 以本地時間日期包含時區偏移量。 例如:

2012-07-27T18:51:45.53403Z         // UTC
2012-07-27T11:51:45.53403-07:00    // Local

默認情況下,Json.NET 保留時區。 您可以通過設置 DateTimeZoneHandling 屬性覆蓋此:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;

如果想要使用Microsoft JSON 日期格式("\/Date(ticks)\/") 設置而不是 ISO 8601 DateFormatHandling上序列化程序設置屬性:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.DateFormatHandling 
= Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat;

編寫縮進的JSON,可以如下設置:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;

JSON屬性名稱使用駝峰式,無需修改數據模型,設置CamelCasePropertyNamesContractResolver序列化程序:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

可以返回一個匿名對象。

XML媒體類型格式化程序

默認XmlMediaTypeFormatter類。默認使用DataContractSerializer來執行序列化。

如果您願意,可以配置XmlMediaTypeFormatter若要使用XmlSerializer而不是DataContractSerializer。 若要執行此操作,設置UseXmlSerializer屬性設置為true:

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.UseXmlSerializer = true;

XmlSerializer類支持一組更窄的類型,而不DataContractSerializer,但提供了更好地控制生成的 XML。 請考慮使用XmlSerializer如果你需要匹配現有 XML 架構。

介紹下DataContractSerializer的序列化行為:

  • 所有公共的屬性和字段進行序列化。若要忽略,使用IgnoreDataMember屬性。
  • 不序列私有和受保護成員。
  • 只讀屬性不會序列化(但是,只讀集合屬性的內容進行序列化)。
  • 類和成員名稱是用XML類聲明中顯示完成相同的。
  • 使用默認XML命名空間。

如果需要更好地控制序列化,您可以修飾的類DataContract屬性。 當存在此屬性時,類被序列,如下所示:

  • "選擇加入"方法: 屬性和字段,默認情況下不序列化。 要序列化的屬性或字段,給它裝飾DataMember屬性。
  • 要序列化的私有或受保護成員,給它裝飾DataMember屬性。
  • 只讀屬性不會序列化。
  • 若要更改類名稱在 XML 中的顯示方式,請設置名稱中的參數DataContract屬性。
  • 若要更改成員名稱在 XML 中的顯示方式,請設置名稱中的參數DataMember屬性。
  • 若要更改的 XML 命名空間,請設置Namespace中的參數DataContract類。

刪除JSON或XML格式化程序

如果您不想要使用它們,可以從列表中的格式化程序,刪除 JSON 格式化程序或 XML 格式化程序。 若要執行此操作的主要原因是:

 

  • 若要限制為特定媒體類型將 web API 響應。 例如,你可能決定以支持僅將 JSON 響應,並刪除 XML 格式化程序。
  • 若要使用自定義格式化程序替換默認的格式化程序。 例如,可以使用您自己的 JSON 格式化程序的自定義實現來替換 JSON 格式化程序。

下面的代碼演示如何刪除默認格式化程序。 調用此成員,從你應用程序_啟動Global.asax 中定義的方法。

void ConfigureApi(HttpConfiguration config)
{
    // Remove the JSON formatter
    config.Formatters.Remove(config.Formatters.JsonFormatter);

    // or

    // Remove the XML formatter
    config.Formatters.Remove(config.Formatters.XmlFormatter);
}

ASP.NET Web API 中的模型驗證

當客戶端將數據發送到 web API 時,通常要執行任何處理之前驗證數據。

數據注釋

在 ASP.NET Web API 中,可以使用中的屬性System.ComponentModel.DataAnnotations命名空間來對模型設置屬性的驗證規則。 請考慮以下模型:

 

using System.ComponentModel.DataAnnotations;

namespace MyApi.Models
{
    public class Product
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
        public decimal Price { get; set; }
        [Range(0, 999)]
        public double Weight { get; set; }
    }
}

如果已在 ASP.NET MVC 中使用模型驗證,這應該很熟悉。 Required屬性指出Name屬性不能為 null。 Range屬性指出Weight必須是介於 0 和 999 之間。

假設客戶端發送 POST 請求,其中以下 JSON 表示形式:

{ "Id":4, "Price":2.99, "Weight":5 }

您可以看到客戶端未包括Name屬性,它被標記為Required。 當 Web API 將轉換為 JSONProduct實例,它會驗證Product針對驗證特性。 在你的控制器操作,可以檢查模型是否有效:

using MyApi.Models;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace MyApi.Controllers
{
    public class ProductsController : ApiController
    {
        public HttpResponseMessage Post(Product product)
        {
            if (ModelState.IsValid)
            {
                // Do something with the product (not shown).

                return new HttpResponseMessage(HttpStatusCode.OK);
            }
            else
            {
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
            }
        }
    }
}

可以創建一個Action篩選器來調用控制器操作前的模型檢查:

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.ModelBinding;

namespace MyApi.Filters
{
    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }
}

如果模型驗證失敗,此篩選器將返回 HTTP 響應,其中包含驗證錯誤。 在這種情況下,不會調用控制器操作。

若要將此篩選器應用於所有的 Web API 控制器,將添加到篩選器的實例HttpConfiguration.Filters在配置期間的集合:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ValidateModelAttribute());

        // ...
    }
}

另一個選項是設置篩選器作為特性上各個控制器或控制器操作:

public class ProductsController : ApiController
{
    [ValidateModel]
    public HttpResponseMessage Post(Product product)
    {
        // ...
    }
}

ASP.NET Web API 中的參數綁定

Web API 在控制器上調用方法時,必須設置參數的值,此過程稱為“綁定”。

默認情況下,Web API 使用以下規則進行參數綁定:

  • 如果參數為“簡單”類型,Web API 會嘗試從 URI 中獲取值。 簡單類型包括 .NET基元類型(int、 bool、 double等),以及 TimeSpan、DateTime、Guid、decimal 和 string,此外還有**借助類型轉換器即可從字符串進行轉換的類型。 (稍后介紹有關類型轉換器的詳細信息。)
  • 對於復雜類型,Web API 嘗試使用媒體類型格式化程序從消息正文中讀取值。

例如,下面是典型的 Web API 控制器方法:

HttpResponseMessage Put(int id, Product item) { ... }

Id 參數是"簡單"類型,因此 Web API 嘗試從請求 URI 中獲取值。 item 參數是復雜類型,因此 Web API 使用媒體類型格式化程序從請求正文中讀取值。

使用 [FromUri]

 若要強制 Web API 從 URI 讀取復雜類型,請向參數添加 [FromUri] 特性。 下面的示例定義 GeoPoint 類型,以及從 URI 獲取 GeoPoint 的控制器方法。

public class GeoPoint
{
    public double Latitude { get; set; } 
    public double Longitude { get; set; }
}

public ValuesController : ApiController
{
    public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }
}

使用 [FromBody]

若要強制 Web API 從請求正文讀取簡單類型,請向參數添加 [FromBody] 特性:

public HttpResponseMessage Post([FromBody] string name) { ... }

 

ASP.NET Web API 中的異常處理

HttpResponseException

如果 Web API 控制器引發未捕獲的異常,則會發生什么情況? 默認情況下,大多數異常將轉換為狀態代碼 500 內部服務器錯誤的 HTTP 響應。

HttpResponseException類型是一種特殊情況。 此異常將返回異常構造函數中指定任何 HTTP 狀態代碼。 例如,以下方法將返回 404,找不到,如果id參數無效。

public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return item;
}

更好地響應的控制,還可以構造的整個響應消息,並將其與包含HttpResponseException:

public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var resp = new HttpResponseMessage(HttpStatusCode.NotFound)
        {
            Content = new StringContent(string.Format("No product with ID = {0}", id)),
            ReasonPhrase = "Product ID Not Found"
        };
        throw new HttpResponseException(resp);
    }
    return item;
}

異常篩選器

您可以自定義 Web API 通過編寫處理異常的方式異常篩選器。 當控制器方法引發任何未處理的異常時執行異常篩選器,除了 HttpResponseException異常。 HttpResponseException類型是一種特殊情況,因為它專為返回的 HTTP 響應。

 

異常篩選器實現System.Web.Http.Filters.IExceptionFilter接口。 編寫異常篩選器的最簡單方法是從派生System.Web.Http.Filters.ExceptionFilterAttribute類並重寫OnException方法。

下面是將轉換的篩選器NotImplementedException異常轉化為 HTTP 狀態代碼 501,未實現:

namespace ProductStore.Filters
{
    using System;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http.Filters;

    public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute 
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            if (context.Exception is NotImplementedException)
            {
                context.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
            }
        }
    }
}

響應的屬性HttpActionExecutedContext對象包含將發送到客戶端的 HTTP 響應消息。

注冊異常篩選器

有幾種方法來注冊 Web API 異常篩選器:

  • 由Action
  • 由控制器
  • 全局

篩選器應用到特定的Action,請將作為屬性的篩選器添加到操作:

public class ProductsController : ApiController
{
    [NotImplExceptionFilter]
    public Contact GetContact(int id)
    {
        throw new NotImplementedException("This method is not implemented");
    }
}

篩選器應用到所有控制器上的操作,將篩選器作為屬性添加到控制器類:

[NotImplExceptionFilter]
public class ProductsController : ApiController
{
    // ...
}

若要對 Web API 的所有控制器全局應用篩選器,將添加到篩選器的實例GlobalConfiguration.Configuration.Filters集合。 在此集合中的異常篩選器適用於任何 Web API 控制器操作。

GlobalConfiguration.Configuration.Filters.Add(
    new ProductStore.NotImplExceptionFilterAttribute());

如果使用"ASP.NET MVC 4 Web 應用程序"項目模板來創建你的項目,將 Web API 配置代碼內的放WebApiConfig類,該類在應用程序位於_開始文件夾:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

        // Other configuration code...
    }
}

HttpError

HttpError對象提供一致的方法來響應正文中返回的錯誤信息。 下面的示例演示如何返回 HTTP 狀態代碼 404 (找不到) 與HttpError響應正文中。

public HttpResponseMessage GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var message = string.Format("Product with id = {0} not found", id);
        return Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
    }
    else
    {
        return Request.CreateResponse(HttpStatusCode.OK, item);
    }
}

CreateErrorResponse是定義在System.Net.Http.HttpRequestMessageExtensions類里的擴展方法。 在內部, CreateErrorResponse創建HttpError實例,然后創建HttpResponseMessage ,其中包含HttpError.

 

HttpError 和模型驗證

public HttpResponseMessage PostProduct(Product item)
{
    if (!ModelState.IsValid)
    {
        return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
    }

    // Implementation not shown...
}

此示例中可能會返回以下響應:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 320

{
  "Message": "The request is invalid.",
  "ModelState": {
    "item": [
      "Required property 'Name' not found in JSON. Path '', line 1, position 14."
    ],
    "item.Name": [
      "The Name field is required."
    ],
    "item.Price": [
      "The field Price must be between 0 and 999."
    ]
  }
}

使用 HttpResponseException HttpError

前面的示例返回HttpResponseMessage消息從控制器操作,但您還可以使用HttpResponseException返回HttpError。 這樣就可以返回強類型化模型中正常成功的情況下,同時仍返回HttpError如果出現錯誤:

public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var message = string.Format("Product with id = {0} not found", id);
        throw new HttpResponseException(
            Request.CreateErrorResponse(HttpStatusCode.NotFound, message));
    }
    else
    {
        return item;
    }
}

全局錯誤處理 ASP.NET Web API 2 中

如今,在Web API中沒有簡單的方式記錄或處理全局錯誤。可以通過異常篩選器過濾某些未經處理的異常,但也有一部分異常篩選器不能處理。例如:

  1. 從控制器的構造函數引發的異常。
  2. 從消息處理程序引發的異常。
  3. 在路由期間引發的異常。
  4. 響應內容序列化期間引發的異常。

我們可以使用異常篩選器,消息處理程序(后面會提到),但是它們都很難處理上面的問題,所以微軟提供了兩個類:IExceptionLogger和IExceptionHandler。

我們該怎么使用:

  • IExceptionLogger用於查看WebAPI捕獲到的所有未經過處理的異常。
  • IExceptionHandler用於自定義所有可能的響應未經處理WebAPI捕獲的異常。
  • IExceptionFilter是Controller或者Action下捕捉未經處理的異常的最簡單的解決方案。
public interface IExceptionLogger
{
   Task LogAsync(ExceptionLoggerContext context, 
                 CancellationToken cancellationToken);
}

public interface IExceptionHandler
{
   Task HandleAsync(ExceptionHandlerContext context, 
                    CancellationToken cancellationToken);
}

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM