zoukankan      html  css  js  c++  java
  • 为ASP生成TypeScript客户端API。净Web API

    介绍 用于开发ASP客户端程序。NET Web API或ASP。NET Core Web API,强类型客户端API生成器用c#代码和TypeScript代码生成强类型客户端API,以最小化重复任务,提高应用程序开发人员的生产力和产品质量。然后,您可以提供或发布生成的源代码或编译的客户端API库给您自己和团队中的其他开发人员或B2B合作伙伴。 本项目提供以下产品: c#中支持桌面、通用Windows、Android和iOS的强类型客户端API代码生成器。用于jQuery、Angular 2+和Aurelia的TypeScript强类型客户端API的代码生成器,以及使用Axios的TypeScript/JavaScript应用。TypeScript代码dom, TypeScript的一个代码dom组件,派生自。net框架的代码dom。POCO2TS。从POCO类生成TypsScript接口的命令行程序。Fonlow。Poco2Ts,一个从POCO类生成TypsScript接口的组件 本文主要关注为jQuery生成TypeScript客户端API。 讲话 对Angular2的支持是在2016年6月的WebApiClientGen v1.9.0 beta版本发布之后,那时Angular2还在RC1版本中。自从WebApiClientGen 2.0版本以来,对Angular 2产品版本的支持就已经可用了。请参阅文章“ASP. net”。NET Web API, Angular2, TypeScript和WebApiClientGen"。 即使您正在进行JavaScript编程,您仍然可以使用WebApiClientGen,因为生成的TypeScript文件可以编译成JavaScript文件。虽然你不会得到设计时类型检查和编译时类型检查,但如果你的JS编辑器支持这些特性,你仍然可以通过源文档享受设计时智能感知。 背景 如果您曾经使用WCF开发过SOAP基础Web服务,那么您可能会喜欢使用SvcUtil.exe或Visual Studio IDE的Web服务引用生成的客户端API代码。当转向Web API时,我觉得自己又回到了石器时代,因为我必须在设计时使用我宝贵的脑力来做大量的数据类型检查,而计算机本应完成这项工作。 2010年,我在IHttpHandler/IHttpModule的基础上开发了一些RESTful Web服务,用于不处理强类型数据,而是处理文档和流等任意数据的Web服务。然而,我得到了更多具有复杂业务逻辑和数据类型的Web项目,我将在SDLC中利用高度抽象和语义数据类型。 我看到ASP。NET Web API通过类ApiController支持高度抽象和强类型函数原型。NET MVC框架可选地提供了很好的生成帮助页面来描述API函数。然而,在开发了Web API之后,我不得不手工编写一些非常原始和重复的客户机代码来使用Web服务。如果Web API是别人开发的,我就必须阅读在线帮助页面,然后编写。 因此,我已经搜索并试图找到一些解决方案,使我能够从编写原始和重复的代码中解脱出来,以便能够集中精力在更高的技术抽象上在客户端构建业务逻辑。以下是协助客户端程序开发的开源项目列表: wadl raml与。net webapiproxy swashbuckle自动rest或data打字打字机 虽然这些解决方案可以生成强类型的客户端代码,并在某种程度上减少重复的任务,但我发现它们都不能给我所有的高效编程经验,我希望: 强类型客户端数据模型映射到服务的数据模型。强类型函数原型映射到ApiController派生类的函数。代码批量生成,就像WCF编程一样。通过使用流行的属性(如DataContractAttribute和JsonObjectAttribute等)的数据注释来选择数据模型。在设计时和编译时进行类型检查。客户端数据模型、功能原型和文档注释的智能感知。 WebApiClientGen来了。 假设 你正在开发ASP。NET Web API 2。x应用程序,并将开发基于AJAX的Web前端JavaScript库,使用jQuery或使用Angular2开发SPA。您和其他开发人员喜欢在服务器端和客户端通过强类型函数实现高抽象,因此使用了TypeScript。POCO类首先由Web api和实体框架代码使用,您可能不想将所有数据类和成员发布到客户机程序。 如果你或者你的团队支持基于主干的开发,那就更好了,因为WebApiClientGen的设计和使用WebApiClientGen的工作流程都考虑了基于主干的开发,这对于持续集成来说比其他分支策略更有效比如特征分支和Gitflow等。 为了跟进这种开发客户端程序的新方法,您最好有一个asp.net。NET Web API项目,或包含Web API的MVC项目。您可以使用现有的项目,也可以创建一个演示项目。 使用的代码 本文主要关注使用jQuery的代码示例。类似的Angular 2+代码示例可以在“ASP”中找到。NET Web API, Angular2, TypeScript和WebApiClientGen”。 步骤0:安装NuGet包WebApiClientGen和WebApiClientGen。jQuery到Web API项目 安装还将安装依赖的NuGet包Fonlow。TypeScriptCodeDOM Fonlow。Poco2Ts到项目引用。 HttpClient helper库应该与生成的代码一起复制到脚本文件夹中,这些代码将在每次执行代码时更新。 另外,用于触发代码根的CodeGenController.cs被添加到项目的Controllers文件夹中。 CodeGenController应该只在调试构建的开发期间可用,因为客户端API应该为每个版本的Web API生成一次。 隐藏,复制Code

    #if DEBUG  //This controller is not needed in production release, 
    	// since the client API should be generated during development of the Web Api.
    ...
    
    namespace Fonlow.WebApiClientGen
    {
        [System.Web.Http.Description.ApiExplorerSettings(IgnoreApi = true)]//this controller is a 
    			//dev backdoor during development, no need to be visible in ApiExplorer.
        public class CodeGenController : ApiController
        {
            /// <summary>
            /// Trigger the API to generate WebApiClientAuto.cs for an established client API project.
            /// POST to  http://localhost:10965/api/CodeGen with json object CodeGenParameters
            /// </summary>
            /// <paramname="parameters"></param>
            /// <returns>OK if OK</returns>
            [HttpPost]
            public string TriggerCodeGen(CodeGenParameters parameters)
            {
    ...
            }
        }

    讲话 CodeGenController安装在YourMvcOrWebApiProject/Controllers中,即使MVC项目的搭建可能有用于ApiController派生类的文件夹API。但是,通常在独立的Web API项目中实现Web API是好的。如果你想让MVC项目和Web API项目在同一个网站上运行,你可以把Web API作为MVC网站的应用程序安装。WebApiClientGenCore 没有安装CodeGenController,你应该复制该文件。 启用Web API的文档注释 在C: YourWebSlnPath Your.WebApi HelpPage App_Start HelpPageConfig领域。cs,有这样一行: 隐藏,复制Code

    //config.SetDocumentationProvider(new XmlDocumentationProvider
    (HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));

    取消注释,使它像这样: 隐藏,复制Code

    config.SetDocumentationProvider(new XmlDocumentationProvider
    (HttpContext.Current.Server.MapPath("~/bin/Your.WebApi.xml")));

    在项目属性页的Build选项卡中,检查Output/XML文档文件并设置“binYour.WebApi”。,而输出路径默认为“bin”。 如果您有用于数据模型的其他程序集,那么您可以执行同样的操作,以确保生成doc注释并将其复制到客户端API。 步骤1:准备JSON配置数据 您的Web API项目可能有POCO类和API函数。[数据模型和ApiController的完整代码示例] 隐藏,收缩,复制Code

    namespace DemoWebApi.DemoData
    {
        public sealed class Constants
        {
            public const string DataNamespace = "http://fonlow.com/DemoData/2014/02";
        }
    
        [DataContract(Namespace = Constants.DataNamespace)]
        public enum AddressType
        {
            [EnumMember]
            Postal,
            [EnumMember]
            Residential,
        };
    
        [DataContract(Namespace = Constants.DataNamespace)]
        public enum Days
        {
            [EnumMember]
            Sat = 1,
            [EnumMember]
            Sun,
            [EnumMember]
            Mon,
            [EnumMember]
            Tue,
            [EnumMember]
            Wed,
            [EnumMember]
            Thu,
            [EnumMember]
            Fri
        };
    
        [DataContract(Namespace = Constants.DataNamespace)]
        public class Address
        {
            [DataMember]
            public Guid Id { get; set; }
    
            public Entity Entity { get; set; }
    
            /// <summary>
            /// Foreign key to Entity
            /// </summary>
            public Guid EntityId { get; set; }
    
            [DataMember]
            public string Street1 { get; set; }
    
            [DataMember]
            public string Street2 { get; set; }
    
            [DataMember]
            public string City { get; set; }
    
            [DataMember]
            public string State { get; set; }
    
            [DataMember]
            public string PostalCode { get; set; }
    
            [DataMember]
            public string Country { get; set; }
    
            [DataMember]
            public AddressType Type { get; set; }
    
            [DataMember]
            public DemoWebApi.DemoData.Another.MyPoint Location;
        }
    
        [DataContract(Namespace = Constants.DataNamespace)]
        public class Entity
        {
            public Entity()
            {
                Addresses = new List<Address>();
            }
    
            [DataMember]
            public Guid Id { get; set; }
    
            
            [DataMember(IsRequired =true)]//MVC and Web API does not care
            [System.ComponentModel.DataAnnotations.Required]//MVC and Web API care about only this
            public string Name { get; set; }
    
            [DataMember]
            public IList<Address> Addresses { get; set; }
    
            public override string ToString()
            {
                return Name;
            }
        }
    
        [DataContract(Namespace = Constants.DataNamespace)]
        public class Person : Entity
        {
            [DataMember]
            public string Surname { get; set; }
            [DataMember]
            public string GivenName { get; set; }
            [DataMember]
            public DateTime? BirthDate { get; set; }
    
            public override string ToString()
            {
                return Surname + ", " + GivenName;
            }
    
        }
    
        [DataContract(Namespace = Constants.DataNamespace)]
        public class Company : Entity
        {
            [DataMember]
            public string BusinessNumber { get; set; }
    
            [DataMember]
            public string BusinessNumberType { get; set; }
    
            [DataMember]
            public string[][] TextMatrix
            { get; set; }
    
            [DataMember]
            public int[][] Int2DJagged;
    
            [DataMember]
            public int[,] Int2D;
    
            [DataMember]
            public IEnumerable<string> Lines;
        }
    
    ...
    ...
    
    namespace DemoWebApi.Controllers
    {
        [RoutePrefix("api/SuperDemo")]
        public class EntitiesController : ApiController
        {
            /// <summary>
            /// Get a person
            /// </summary>
            /// <paramname="id">unique id of that guy</param>
            /// <returns>person in db</returns>
            [HttpGet]
            public Person GetPerson(long id)
            {
                return new Person()
                {
                    Surname = "Huang",
                    GivenName = "Z",
                    Name = "Z Huang",
                    BirthDate = DateTime.Now.AddYears(-20),
                };
            }
    
            [HttpPost]
            public long CreatePerson(Person p)
            {
                Debug.WriteLine("CreatePerson: " + p.Name);
    
                if (p.Name == "Exception")
                    throw new InvalidOperationException("It is exception");
    
                Debug.WriteLine("Create " + p);
                return 1000;
            }
    
            [HttpPut]
            public void UpdatePerson(Person person)
            {
                Debug.WriteLine("Update " + person);
            }
    
            [HttpPut]
            [Route("link")]
            public bool LinkPerson(long id, string relationship, [FromBody] Person person)
            {
                return person != null && !String.IsNullOrEmpty(relationship);
            }
    
            [HttpDelete]
            public void Delete(long id)
            {
                Debug.WriteLine("Delete " + id);
            }
    
            [Route("Company")]
            [HttpGet]
            public Company GetCompany(long id)
            {

    下面的JSON配置数据是发布到CodeGen Web API: 隐藏,收缩,复制Code

    {
        "ApiSelections": {
            "ExcludedControllerNames": [
                "DemoWebApi.Controllers.Account",
                "DemoWebApi.Controllers.FileUpload"
            ],
    
            "DataModelAssemblyNames": [
                "DemoWebApi.DemoData",
                "DemoWebApi"
            ],
    
            "CherryPickingMethods": 3
        },
    
        "ClientApiOutputs": {
            "ClientLibraryProjectFolderName": "..\DemoWebApi.ClientApi",
            "GenerateBothAsyncAndSync": true,
            "CamelCase": true,
    
            "Plugins": [
                {
                    "AssemblyName": "Fonlow.WebApiClientGen.jQuery",
                    "TargetDir": "Scripts\ClientApi",
                    "TSFile": "WebApiJQClientAuto.ts",
                    "AsModule": false,
                    "ContentType": "application/json;charset=UTF-8"
                },
    
                {
                    "AssemblyName": "Fonlow.WebApiClientGen.NG2",
                    "TargetDir": "..\DemoNGCli\NGSource\src\ClientApi",
                    "TSFile": "WebApiNG2ClientAuto.ts",
                    "AsModule": true,
                    "ContentType": "application/json;charset=UTF-8"
                }
            ]
    
    
        }
    }

    建议将JSON配置数据保存到Web API项目文件夹中的这样一个文件中。 如果在Web API项目中定义了所有POCO类,那么应该将Web API项目的程序集名称放入“DataModelAssemblyNames”数组中。如果您有一些专门的数据模型程序集来实现良好的关注点分离,那么应该将相应的程序集名称放入数组中。 “TypeScriptNG2Folder”是Angular2项目的绝对路径或相对路径。例如,“. .表示Angular2项目是作为Web API项目的兄弟项目创建的。 CodeGen根据“CherryPickingMethods”从POCO类中生成强类型的TypeScript接口,“CherryPickingMethods”在下面的文档注释中描述: 隐藏,收缩,复制Code

    /// <summary>
    /// Flagged options for cherry picking in various development processes.
    /// </summary>
    [Flags]
    public enum CherryPickingMethods
    {
        /// <summary>
        /// Include all public classes, properties and properties.
        /// </summary>
        All = 0,
    
        /// <summary>
        /// Include all public classes decorated by DataContractAttribute,
        /// and public properties or fields decorated by DataMemberAttribute.
        /// And use DataMemberAttribute.IsRequired
        /// </summary>
        DataContract =1,
    
        /// <summary>
        /// Include all public classes decorated by JsonObjectAttribute,
        /// and public properties or fields decorated by JsonPropertyAttribute.
        /// And use JsonPropertyAttribute.Required
        /// </summary>
        NewtonsoftJson = 2,
    
        /// <summary>
        /// Include all public classes decorated by SerializableAttribute,
        /// and all public properties or fields
        /// but excluding those decorated by NonSerializedAttribute.
        /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
        /// </summary>
        Serializable = 4,
    
        /// <summary>
        /// Include all public classes, properties and properties.
        /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
        /// </summary>
        AspNet = 8,
    }

    默认选项是DataContract。你可以使用任何或组合的方法。 第2步:运行Web API项目的调试构建,并发布JSON配置数据来触发客户端API代码的生成 在IIS Express的IDE中运行Web项目。 然后使用Curl或Poster或任何您喜欢的客户机工具向http://localhost:10965/api/codegen发布消息,并使用content-type=application/json。 提示 因此,基本上,您只需要第2步在Web API更新时生成客户机API,因为您不需要每次都安装NuGet包或编写新的JSON配置数据。 编写一些批处理脚本来启动Web API并发布JSON配置数据应该不难。实际上,为了方便起见,我已经起草了一个脚本:一个Powershell脚本文件,它在IIS Express上启动Web (API)项目,然后发布JSON配置文件来触发代码生成。 发布客户端API库 现在你已经在TypeScript客户端API生成,类似于这个例子: 隐藏,收缩,复制Code

    /// <reference path="../typings/jquery/jquery.d.ts" />
    /// <reference path="HttpClient.ts" />
    namespace DemoWebApi_DemoData_Client {
        export enum AddressType {Postal, Residential}
    
        export enum Days {Sat=1, Sun=2, Mon=3, Tue=4, Wed=5, Thu=6, Fri=7}
    
        export interface Address {
            Id?: string;
            Street1?: string;
            Street2?: string;
            City?: string;
            State?: string;
            PostalCode?: string;
            Country?: string;
            Type?: DemoWebApi_DemoData_Client.AddressType;
            Location?: DemoWebApi_DemoData_Another_Client.MyPoint;
        }
    
        export interface Entity {
            Id?: string;
            Name: string;
            Addresses?: Array<DemoWebApi_DemoData_Client.Address>;
        }
    
        export interface Person extends DemoWebApi_DemoData_Client.Entity {
            Surname?: string;
            GivenName?: string;
            BirthDate?: Date;
        }
    
        export interface Company extends DemoWebApi_DemoData_Client.Entity {
            BusinessNumber?: string;
            BusinessNumberType?: string;
            TextMatrix?: Array<Array<string>>;
            Int3D?: Array<Array<Array<number>>>;
            Lines?: Array<string>;
        }
    }
    
    namespace DemoWebApi_DemoData_Another_Client {
        export interface MyPoint {
            X?: number;
            Y?: number;
        }
    }
    
    namespace DemoWebApi_Controllers_Client {
    
        export class Entities {
            httpClient: HttpClient;
            constructor(public baseUri?: string, public error?: 
            (xhr: JQueryXHR, ajaxOptions: string, thrown: string) => 
            any, public statusCode?: { [key: string]: any; }){
                this.httpClient = new HttpClient();
            }
    
            /**
             * Get a person
             * GET api/Entities/{id}
             * @param {number} id unique id of that guy
             * @return {DemoWebApi_DemoData_Client.Person} person in db
             */
            GetPerson(id: number, callback: (data : DemoWebApi_DemoData_Client.Person) => any){
                this.httpClient.get(encodeURI(this.baseUri + 
                'api/Entities/'+id), callback, this.error, this.statusCode);
            }
    
            /**
             * POST api/Entities
             * @param {DemoWebApi_DemoData_Client.Person} person
             * @return {number}
             */
            CreatePerson(person: DemoWebApi_DemoData_Client.Person, 
            	callback: (data : number) => any){
                this.httpClient.post(encodeURI(this.baseUri + 
                'api/Entities'), person, callback, this.error, this.statusCode);
            }
    
            /**
             * PUT api/Entities
             * @param {DemoWebApi_DemoData_Client.Person} person
             * @return {void}
             */
            UpdatePerson(person: DemoWebApi_DemoData_Client.Person, callback: (data : void) => any){
                this.httpClient.put(encodeURI(this.baseUri + 
                'api/Entities'), person, callback, this.error, this.statusCode);
            }
    
            /**
             * DELETE api/Entities/{id}
             * @param {number} id
             * @return {void}
             */
            Delete(id: number, callback: (data : void) => any){
                this.httpClient.delete(encodeURI(this.baseUri + 
                'api/Entities/'+id), callback, this.error, this.statusCode);
            }
        }
    
        export class Values {
            httpClient: HttpClient;
            constructor(public baseUri?: string, public error?: 
            (xhr: JQueryXHR, ajaxOptions: string, thrown: string) => any, 
            public statusCode?: { [key: string]: any; }){
                this.httpClient = new HttpClient();
            }
    
            /**
             * GET api/Values
             * @return {Array<string>}
             */
            Get(callback: (data : Array<string>) => any){
                this.httpClient.get(encodeURI(this.baseUri + 
                'api/Values'), callback, this.error, this.statusCode);
            }
    
            /**
             * GET api/Values/{id}?name={name}
             * @param {number} id
             * @param {string} name
             * @return {string}
             */
            GetByIdAndName(id: number, name: string, callback: (data : string) => any){
                this.httpClient.get(encodeURI(this.baseUri + 
                'api/Values/'+id+'?name='+name), 
                callback, this.error, this.statusCode);
            }
    
            /**
             * POST api/Values
             * @param {string} value
             * @return {string}
             */
            Post(value: {'':string}, callback: (data : string) => any){
                this.httpClient.post(encodeURI(this.baseUri + 
                'api/Values'), value, callback, this.error, this.statusCode);
            }
    
            /**
             * PUT api/Values/{id}
             * @param {number} id
             * @param {string} value
             * @return {void}
             */
            Put(id: number, value: {'':string}, callback: (data : void) => any){
                this.httpClient.put(encodeURI(this.baseUri + 
                'api/Values/'+id), value, callback, this.error, this.statusCode);
            }
    
            /**
             * DELETE api/Values/{id}
             * @param {number} id
             * @return {void}
             */
            Delete(id: number, callback: (data : void) => any){
                this.httpClient.delete(encodeURI(this.baseUri + 
                'api/Values/'+id), callback, this.error, this.statusCode);
            }
        }
    }

    提示 1. 如果您希望生成的TypeScript代码符合JavaScript和JSON的驼峰式外壳,您可以在Web API搭建代码的类WebApiConfig中添加以下一行: 隐藏,复制Code

    config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = 
         new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

    那么属性名和函数名将使用驼峰格式,如果c#中的名称使用Pascal格式。详情请查看camel肠衣或pascal肠衣。 2. PowerShell脚本还将TS文件编译为JS文件。 , 内部使用 在某些dec中编写客户端代码时像Visual Studio这样的文本编辑器,你可能会得到很好的智能感知。 外部使用 如果您希望一些外部开发人员通过JavaScript使用您的Web API,您可以发布生成的TypeScript客户端API文件或编译的JavaScript文件,以及由ASP生成的帮助页面。净MVC框架。 的利益点 而ASP。NET MVC和Web API使用NewtonSoft。Json用于Json应用程序,NewtonSoft。Json可以很好地处理由DataContractAttribute修饰的POCO类。 CLR命名空间将被转换为TypeScript命名空间,方法是用下划线代替dot,并添加“Client”作为后缀。例如,名称空间My.Name。space将被转换为My_Name_space_Client。 从某种角度来看,服务名称空间/函数名与客户端名称空间/函数名之间的一对一映射暴露了服务的实现细节,通常不建议这样做。然而,传统的RESTful客户机编程要求程序员知道服务功能的URL查询模板,而查询模板是服务的实现细节。因此,这两种方法在某种程度上公开了服务的实现细节,但结果不同。 对于客户端开发人员,经典的功能原型如下: 隐藏,复制Code

    ReturnType DoSomething(Type1 t1, Type2 t2 ...)

    是API函数,其余是传输的技术实现细节:TCP/IP、HTTP、SOAP、面向资源、基于crud的uri、RESTful、XML和JSON等。函数原型和一段API文档对于调用API函数应该足够好了。客户端开发人员不应该关心传输的那些实现细节,至少在操作成功时是这样。只有在出现错误时,开发人员才需要关心技术细节。例如,在基于SOAP的web服务中,您必须了解SOAP故障;在RESTful Web服务中,您可能必须处理HTTP状态码和响应。 并且查询模板很少给出API函数的语义意义。相比之下,WebApiClientGen用服务函数来命名客户端函数,就像WCF中的SvcUtil.exe在默认情况下做的那样,因此,只要服务开发人员用良好的语义名称来命名服务函数,所生成的客户端函数就具有良好的语义意义。 在涵盖服务开发和客户端开发的SDLC的大框架中,服务开发人员了解服务功能的语义含义,通常在功能描述之后命名函数是一种良好的编程实践。面向资源的CRUD可能具有语义意义,也可能只是功能描述的技术转换。 WebApiClientGen将您的Web API的文档注释复制到生成的TypeScript代码中的JsDoc3注释中,因此您几乎不需要阅读MVC生成的帮助页面,并且您的客户端服务编程将变得更加无缝。 许多JavaScript框架,如React和Vue.js没有自带内置的HTTP请求库,而是依赖于第三个库,如Axios。因为Axios显然是近年来在JavaScript程序员中最流行的,React和Vue都推荐它。js,支持Axios可能更可行。 提示 编写脚本来自动化一些步骤以实现持续集成并不困难。你可以在 webapiclientgen示例。net核心演示 找到这些“Create*ClientApi”。在根文件夹中的ps1”文件。 讲话 开发Web服务和客户机的环境一直在迅速变化。自从WebApiClientGen在2015年9月发布第一个版本以来,就出现了开放API定义格式,该格式由2015年11月成立的开放API Initiative运行。希望这个项目能够解决Swagger规范2.0的一些不足之处,特别是在处理小数/货币类型方面。然而,利用WebApiClientGen的SDLC是经过优化的,可以在ASP上开发客户端程序。NET Web API和ASP。NET Core Web API。 参考文献 为什么打印稿? , 本文转载于:http://www.diyabc.com/frontweb/news17312.html

  • 相关阅读:
    多表查询,连表查询
    mysql数据概念难点
    mysql练习题
    linux下 redis
    nginx安装
    八皇后问题 OpenJ_Bailian
    Prime Ring Problem hdu-1016 DFS
    Oil Deposits hdu-1241 DFS
    Highways
    畅通工程再续
  • 原文地址:https://www.cnblogs.com/Dincat/p/13494035.html
Copyright © 2011-2022 走看看