Web API——添加Swagger、SQL Server、日志记录、导出到Excel和Docker

目录

介绍

要求

1.创建一个新的Web API项目

2.将Swagger添加到项目

3.将项目连接到SQL Server数据库

4.登录到SQL Server

5.添加“导出到Excel”

6.对Docker的支持

在本地运行Docker镜像

摘要


介绍

2016年发布以来,ASP.NET Core取得了惊人的进展,而且每个版本似乎都变得更好,更友好且对开发人员更友好。

Web API是对旧的WCF服务的巨大改进(并且甚至不谈论SOAP ...),并且那里有大量的入门资源。本文只是重新编写轮子而已,它只是您要做的以下步骤的备忘单

  1. 创建一个新的Web API项目
  2. Swagger添加到您的项目
  3. 将项目连接到SQL Server数据库
  4. 将日志添加到项目中,以便所有异常或日志消息都保存在SQL Server表中(而不是转储到IIS .txt日志文件中的某个位置)
  5. CRUDE添加到您的项目。不,那不是错字。我们将拥有常规的Create-Read-Update-Delete函数,以及...只是为了展示它的简单性....导出。我们将添加一个端点,使用Open XML库将数据导出到真实的.xlsx文件,仅此而已。
  6. Web API作为Docker镜像部署到Azure,然后在本地和Azure容器中运行该镜像。

除了第五项之外,这是我使用每个Web API都要执行的4个步骤,因此我认为最好尽可能快速、轻松地记录您需要执行的步骤,这样我们有更多时间专注于完成实际工作!

现在,我已经提供了一个含有从本文的示例代码文件MikesBank.zip,但我强烈建议你使用它。

Visual Studio 2017ASP.NET Core几乎每个月都在变化...与使用示例相比,使用Microsoft在阅读本文时提供的任何新模板,从头开始创建自己的项目并遵循这些说明是一个更好的主意。我已经提供了,到您阅读完本段时,它可能已经过时了一半。

祝您阅读愉快!

要求

要阅读本文,您将需要:

  • C#知识
  • Visual Studio 2017的副本
  • 安装的.NET Core v2.2 SDK(或更高版本)
  • SQL Server Management Studio(或者您可以修改连接字符串,以指向您自己的数据库风格)

我建议您先按照以下说明更新 VS2017.NET Core的副本。在撰写本文时,我尝试导入一个nuget包,VS2017很高兴地接受了该包...但随后抛出了许多编译错误,因为nuget包比我的.NET Core版本更新了,拒绝使用我的旧版本。

1.创建一个新的Web API项目

这个问题已经讨论了很多次了,我相信大家都知道该怎么做。

如果您使用的是Visual Studio 2017

1、启动Visual Studio 2017,然后选择File\New\Project

2、在Visual C\.NET Core下,选择ASP.NET Core Web应用程序,并为您的项目命名(我要称呼我的名字MikesBank),然后单击OK

3、在下一个屏幕上,确保已选择API,它是一个.NET Core应用程序,然后单击确定

如果您使用的是Visual Studio 2019

  1. 单击文件\新建\项目
  2. 在第一个屏幕上,单击创建新项目 ”
  3. 在第二个屏幕上,单击ASP.NET Core Web应用程序
  4. 在第三个屏幕上,让我们输入MikesBank的项目名称,然后单击Create
  5. 在第四屏幕上,选择API ”
  6. 请注意,Visual Studio 2019不再询问您是否要默认创建新的git存储库。但是,在VS2019窗口的右下角,可以单击添加到源代码管理 ”按钮。如果选择此选项,然后选择Git,则现在可以获取VS2019将源代码推送到Azure中对DevOps友好的项目中。这是添加持续集成管道工作项面板的绝佳起点。这很棒....

因此,... Visual Studio将为您创建一个基本的API项目,该项目将返回一些硬编码的数据。

如果您在Chrome中运行该项目,则会看到几项JSON数据。

但是,如果您仍在使用Internet Explorer,则可能会收到一条奇怪的消息,询问您是否要从本地主机打开或保存values.json19个字节)? ”是的,这是2019年,而他们仍在这样做。我不知道为什么

作为开发人员,您可以(并且应该)通过添加StackOverflow2010年开始的文章中描述的注册表项来解决此问题。这将使Internet Explorer实际上显示JSON数据,而不是困扰我们。

因此,我们现在有了Web API的起点。现在,让我们变得更好!

2.Swagger添加到项目

考虑到Visual Studio对开发人员的友好程度,当我们告诉我们正在创建Web API项目时,没有提供为我的API创建Swagger页面作为选项,我总是感到惊讶。但是,添加起来很容易。

要将Swagger添加到您的项目中:

1、右键单击您的项目名称,然后选择管理NuGet软件包... ”

2、点击浏览标签,然后搜索并安装swashbuckle.aspnetcore

3、现在,您可以关闭此“nuget软件包管理器屏幕。

4、打开startup.cs文件,并对Configure()函数进行以下更改:

app.UseSwagger();

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json","MikesBank API");
    c.RoutePrefix = string.Empty;
});

5、请注意,RoutePrefix...将使Swagger页面成为默认页面。因此,当我调试时,URL指向根路径时,将显示Swagger页面,例如:

https://localhost:44350/

6、接下来,我将可视的修改构造函数,并添加一个新的env变量:

public Startup(IHostingEnvironment env, IConfiguration configuration)
{
    Configuration = configuration;
    this.env = env;
}

private IHostingEnvironment env { get; }

有了这个,我们可以对ConfigureServices()函数进行更改:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    var pathIncludeXmlComments =
        $@"{env.ContentRootPath}\{env.ApplicationName}.xml";
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
        {
            Version = "v1",
            Title = "MikesBank API",
            Description = "For CodeProject"
        });

        if (System.IO.File.Exists(pathIncludeXmlComments))
            c.IncludeXmlComments(pathIncludeXmlComments);
    });
}

这有点肿,但是可以解决Swagger的问题。在我们的Web API控制器中添加注释通常很有用,而Swagger将在其网页中显示这些注释。但是,如果此.XML文件不存在,它会完全崩溃我们的应用程序,我只在这个.xml文件存在的情况下才添加注释。

更新(20203月):
对于最新版本的Swagger,您需要用以下用法定义SwaggerDoc

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "MikesBank API", Version = "v1" });

    if (System.IO.File.Exists(pathIncludeXmlComments))
        c.IncludeXmlComments(pathIncludeXmlComments);
});

7、代码更改均已完成,但是现在,我们只需要对项目设置进行一些更改。再次右键单击您的项目名称,然后选择属性。然后,选择构建选项卡。

8、在错误和警告部分,将;1591添加到要忽略的警告列表中。如果我们选择在Web API控制器中添加注释,那就太好了!但是,如果我们还没有添加注释,我不喜欢Visual Studio为我们的代码添加下划线。这不是错误/警告,因此我忽略此警告以忽略此类警告。

1701;1702;1591

9、仍在此屏幕上的输出部分中,选中XML文档文件:复选框。

10、现在,跳至调试选项卡,然后从启动浏览器文本框中删除api/values,使其保持空白。

如果现在运行项目,您将看到Swagger网站,以及VS2017为我们创建的示例端点的列表。

看起来不错!如果要运行简单的获取所有值函数,可以单击第一个GET行,单击试用按钮,然后单击执行按钮,然后将看到带有两个内容的Response正文与以前一样,采用硬编码的值。

好的,它不像PostmanFiddler那样复杂,但是它是免费的,友好的并且非常有用。

Swagger在此页面上包含评论就像在端点上方添加summaryremarks部分一样简单:

/// <summary>
/// This is the Summary, describing the endpoint
/// </summary>
/// <remarks>
/// These are the Remarks for the endpoint
/// </remarks>
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    return new string[] { "value1", "value2" };
}

请记住,Swagger本身确实会定期更新,并且(再一次),在您阅读本文时,或者如果您在google上搜索Swagger代码没有构建/显示时出现的问题,请务必检查最新的文档。

3.将项目连接到SQL Server数据库

好的,现在让我们将Web API链接到SQL Server数据库。为此,请再次进入nuget软件包管理器,搜索并安装以下三个软件包:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.EntityFrameworkCore.SqlServer.Design

如果您喜欢剪切而不是搜索,那么另一种方法是单击Tools \ Nuget Package Manager \ Package Manager Console,然后运行以下3个命令:

install-package Microsoft.EntityFrameworkCore.SqlServer
install-package Microsoft.EntityFrameworkCore.Tools
install-package Microsoft.EntityFrameworkCore.SqlServer.Design

无论哪种方式,这都会添加连接到SQL Server所需的3个程序包。

201910月更新

我确实警告过您,这些东西进展很快...自从发布本文(上个月!)以来,情况已经发生了变化。如果您尝试下载这三个软件包的最新版本,则将下载仅与.Net Core 3.x兼容的版本。并且此版本的.Net Core仅在使用VS2019而不是 VS2017)时才有效。所以——如果您正在运行Visual Studio 2017,请确保选择这些软件包的稍早版本,因为它们将与.Net Core 2.x兼容。

(微软:将NuGet包管理器屏幕更改为只显示与.Net Core版本兼容的包是很有帮助的,用户选择用.Net Core来编写他们的应用程序。而不是建议他们你自己的依赖项部分已经显示的版本与他们的项目不兼容

现在,我更喜欢使用数据库优先方法,在该方法中,我已经有了一个实时运行的数据库,然后将其链接到我的Web API。在这篇文章中,我已经创建了我的本地服务器上的SQL Server数据库称为Southwind,它包含四个表,LocationDepartmentEmployeeLogging

如果您想继续学习,我提供了一个Southwind.sql脚本,它将为您创建该数据库、表和数据。

实际上,我们可以使Visual Studio根据这些表的结构为我们创建类。为此,请打开软件包管理器控制台(单击工具\Nuget软件包管理器\软件包管理器控制台),然后输入以下命令,将您的数据库的连接字符串替换为我的连接字符串:

Scaffold-DbContext "Server=localhost;Database=Southwind;
Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

这将为我们神奇地创建一个SouthwindContext类,以及四个类,每个数据库表一个。请注意,如果需要,您可以只要求一些(但不是所有)数据库表创建类,方法是使用——Tables参数,然后列出表名

Scaffold-DbContext "Server=localhost;Database=Southwind;
Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer
-OutputDir Models -Tables "Course","Lesson","User"

另请注意,如果您的代码当前未事先成功构建,则此Scaffold-DbContext命令将不起作用。因此,在运行该命令之前,请检查您的代码是否可以正常构建。

您可以在此处查看其他选项。

接下来,我们需要告知我们的appsettings.json文件有关数据库连接的信息,因此请打开此文件,并将其添加到顶部:

"ConnectionStrings": {
  "SouthwindDatabase": "Server=.;Database=Southwind;Trusted_Connection=True;"
},

现在,打开startup.cs,并将其添加到ConfigureServices()函数中:

var ConnectionString = Configuration.GetConnectionString("SouthwindDatabase");
services.AddDbContext<SouthwindContext>(options =>
    options.UseSqlServer(ConnectionString)
);

在继续之前,我们需要确保此代码成功构建,但是现在,由于缺少两个using语句,因此不会成功。您可以在Startup.cs顶部手动添加它们:

using Microsoft.EntityFrameworkCore;
using MikesBank.Models;

...或者您可以在上面的代码中单击SouthwindContextUseSqlServer,然后使用“ CTRL” +“.”使Visual Studio为您添加using语句。

如果您尝试再次构建,那么它现在应该可以成功构建,我们可以继续。

现在,我们具有将数据库CRUD操作添加到Web API的所有构造块。

删除Controllers文件夹中的ValuesController.cs文件,然后右键单击Controllers文件夹,选择Add\Controller,选择最后一个选项使用实体框架进行操作的API Controller ”,然后单击Add

现在,您可以选择一个模型,并获取Visual Studio为您创建一组端点。我将选择Employee模型:

然后单击添加按钮,就这样,我们为其中一个表设置了一组CRUD端点。

我们甚至可以进入控制器文件(在我的示例中为EmployeeController.cs),然后修改注释,使其对Swagger更友好。只需删除现有注释,然后将光标放在[HttpGet]上方的空白行上,键入///Visual Studio将为您提供占位符以键入您的注释。

/// <summary>
/// Load a list of all employee records from the database
/// </summary>
/// <returns>
/// An enumerable list of Employee records
/// </returns>
[HttpGet]
public async Task<ActionResult<IEnumerable<Employee>>> GetEmployee()
{
  return await _context.Employee.ToListAsync();
}

如果现在运行项目,将显示Swagger页面,我们可以为其选择GET端点Employees,并获取所有employees列表。

(个人)会在这一点上做一个小小的改变:通常情况下,当我得到Employee记录清单时,我不希望包括每条记录的整个层次(其中每个employee属于department,并且department是在location中),我只想要Employee记录,仅此而已。

您可以通过打开Employee.cs文件并在virtual字段之前添加[JsonIgnore]来防止序列化整个层次结构(您还需要使用CTRL +“.”技巧来获取Visual Studio为此添加一条using语句)

[JsonIgnore]
public virtual Department Dep { get; set; }
[JsonIgnore]
public virtual Employee EmpManager { get; set; }
[JsonIgnore]
public virtual ICollection<Employee> InverseEmpManager { get; set; }

您还可以在Startup.cs更改AddMvc()设置,以防止JSON.Net因获取过多父项/所有者的每个Employee记录:

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .AddJsonOptions(
        options => options.SerializerSettings.ReferenceLoopHandling =
                               Newtonsoft.Json.ReferenceLoopHandling.Ignore
    );

进行此更改后,我仅收到Employee记录列表,仅此而已。

这就是我们使Web API连接到SQL Server数据库以及将数据读取/写入SQL Server数据库的方式。我很乐意为此邀功,但是正如您所看到的,Visual Studio为我们做了(几乎)所有工作。我们只需要记住要采取的步骤。

4.登录到SQL Server

现在,当发生错误/异常时,我真的不喜欢它,并且错误字符串只是进入存储在IIS服务器上某个位置的一些随机.txt文件。将消息发送到SQL Server上的Logging表要有用得多,因此我们可以跟踪此类问题,也许可以在日志查看器屏幕上列出这些问题,以供管理员跟踪,保存堆栈跟踪等等。

当然,有一个陷阱:如果由于无法连接数据库而引发了异常,那么…………肯定不会将异常消息存储在数据库中,因为它可以找不到!

当然,我们可以联系第三方(例如nLog)来处理日志,但是就我个人而言,我更喜欢自己做。

首先,如您所见,我的SQL Server数据库中有一个Logging表。

没有什么复杂的。Log_Severity,在(异常)Log_MessageLog_StackTrace字段将全部来自刚刚发生的任何异常,我有一个Log_Source字段,我们可以填充应用程序抛出异常的哪个区域。

哦,我的Update_Time字段(每个表中都有一个)始终包含UTC timezone中的日期时间。我们很可能在不同国家/地区拥有用户,他们希望在当地时间知道何时发生异常。

要在我们的代码中使用此(或任何其他)表结构,这是我们需要做的。

  1. 在我们的项目中创建一个名为LogProvider的新文件夹。
  2. 我提供了一个LogProvider.zip文件,将.zip文件中的4个文件解压缩到该文件夹​​中。
  3. 在每个文件中,名称空间当前都设置为MikesBank.LogProvider。您需要将其更改为自己的名称空间(取决于您为项目命名的名称)。
  4. SqlHelper.csDBLogger.cs文件中,特定于我的Logging数据库表的代码。如果表具有不同的名称或字段,则需要更改此代码。
  5. Startup.cs,添加using MikesBank.LogProvider;
  6. Configure()函数中,我们需要注入一个额外的依赖项:
public void Configure(IApplicationBuilder app,
                     IHostingEnvironment env, ILoggerFactory loggerFactory)

现在,我们可以在Configure()函数中添加以下几行:

loggerFactory.AddConsole(Configuration.GetSection("Logging"));
//  The following "AddContext" comes from our DBLoggerExtensions class.
//  We will log any errors of Information of higher.
//  (Trace=0, Debug=1, Information=2, Warning=3, Error=4, Critical=5, None=6)
loggerFactory.AddContext(LogLevel.Information,
             Configuration.GetConnectionString("SouthwindDatabase"));

此时,如果您尝试构建项目,可能会收到一条错误消息,说其中ILoggerFactory不包含AddContext的定义。要解决此问题,我们需要告诉它扩展方法在哪里。在Startup.cs的顶部,添加以下行:

using MikesBank.LogProvider;

现在,让我们开始吧。

现在,在EmployeesController.cs,可以添加日志记录。为此,我需要添加一个新变量:

private readonly ILogger logger;

以及新的using声明...

using Microsoft.Extensions.Logging;

然后,我可以修改构造函数:

public EmployeesController(SouthwindContext context, ILoggerFactory loggerFactory)
{
    _context = context;
    logger = loggerFactory.CreateLogger<EmployeesController>();
}

就是这样!

现在,您可以很高兴地得到你想要的LogInformationLogWarningLogErrors。例如:

[HttpGet]
public async Task<ActionResult<IEnumerable<Employee>>> GetEmployee()
{
    logger.LogInformation("Loading a list of Employee records");
    return await _context.Employee.ToListAsync();
}

只是一个烦人的问题。运行此代码后,并调用GET终点,我在我的Logging表中得到了加载员工的列表记录信息,但我也能从幕后得到很多信息。就个人而言,我发现这些内容使查找我真正感兴趣的Log消息变得异常困难,并且希望将其关闭。

为此,您可以进入appsettings.Development.json文件,并修改MicrosoftSystem库中将包括的日志消息类型。如果将它们更改为Warning,则您的日志将不会填满所有这些额外的Entity Framework消息。

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Warning",
      "Microsoft": "Warning"
    }
  }
}

显然,这是可选的,也许您会希望在开发版本中看到这些详细信息。由你决定。

最后一件事。

我对某些API的最大烦恼之一是可怕的“HTTP响应500:内部服务器错误。当您自己的API抛出此异常时,通常是因为出了点问题,您的代码也没有费心去解决问题或优雅地处理问题。

而且,当然,因为您尚未捕获到异常,所以您当然不会尝试将其发送到日志,以便您的开发人员和支持团队可以调查原因。因此,请将每个端点包装在try...catch中,并确保所有异常消息最终出现在Logging表中。

这很容易做到,但以后会省很多烦恼。

public async Task<ActionResult<IEnumerable<Employee>>> GetEmployee()
{
    try
    {
        logger.LogInformation("Loading a list of Employee records");
        return await _context.Employee.ToListAsync();
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "An exception occurred in the GetEmployee() endpoint");
        return new BadRequestObjectResult(ex.Message);
    }
}

显然,在生产版本中,您可能不希望如上所述返回完整的异常消息,并且可以根据需要进行修改。

5.添加导出到Excel”

是的,我知道...很可能,我们没人会添加Web API端点,该端点返回一个原始Excel文件,其中包含来自一个表的所有数据。但是,我们已经走了这么远,而且因为它是如此的简单,所以让我们看看我们将如何做到。如果没有其他要求,这对您的其他ASP.NET Core项目很有用。

首先,我们需要最后一次进入NuGet软件包管理器,并安装DocumentFormat.OpenXml软件包。即使我们在服务器上没有Excel,这也使我们可以创建Excel文件(* .xlsx)。

接下来,在项目中创建一个名为Helpers的文件夹,并将附加的CreateExcelFile.cs保存到该文件夹​​中。

有了此文件后,我们就可以开始了。

要向我们的控制器添加一个Export端点,就像加载数据,然后调用该StreamExcelDocument函数,将要导出的数据以及要使用的文件名一样简单:

[HttpGet("ExportToExcel")]
public async Task<IActionResult> ExportEmployeesToExcel()
{
    try
    {
        List<Employee> employees = await _context.Employee.ToListAsync();
        FileStreamResult fr = ExportToExcel.CreateExcelFile.StreamExcelDocument
                             (employees, "Employees.xlsx");
        return fr;
    }
    catch (Exception ex)
    {
        return new BadRequestObjectResult(ex);
    }
}

多么简单!

作为一个在金融行业工作的人,让我告诉你,有一个简单的,可重复使用的导出到Excel功能的黄金。这是我的客户每次都要求的第一个功能……他们喜欢自己的Excel

6.Docker的支持

当我第一次开始研究Docker支持时,我真的以为这很简单。毕竟,当您在Visual Studio 20172019中创建项目时,它会询问您是否需要Docker支持。从那里开始,使用Publish将应用程序作为Docker镜像发布到Azure 真的很容易。因此,这必须非常容易。

不是。

首先,当您创建项目并说您确实想要Docker支持时,您可能会说您想要Windows(而不是Linux)支持它。这会为您创建一个名为Dockerfile的文件,但是(在撰写本文时)Azure 不支持它提供的.NET Core版本。这样,当您发布到Azure时,您将在Azure门户中看到作为新的容器注册表的应用列表....但是Azure将无法运行它。

实际上,有一个选项可以运行此注册表的实例,但是实际上发现这个选项还不是很清楚。

更糟糕的是,如果出现问题,错误消息几乎毫无用处。

这是什么意思?

若要找出导致错误的原因,您需要单击Azure窗口右上方的“>”按钮,然后运行以下命令:

az group deployment operation list
--resource-group <YourResourceGroup> --name Microsoft.ContainerInstance

然后,这将向您显示冗长的JSON消息,其中包含错误:

(我把这条冗长的错误信息ps成两行,这样你就可以很容易地读出来了。)

好吧。因此,Visual Studio为我们创建了一个容器注册表,但是使用了它实际上不支持的Windows版本。我确定可以提高此功能的可用性。

造成此问题的原因似乎是Visual Studio为我们创建的Dockerfile文件。它提到的dotnet核心SDK和运行时版本高于Azure支持的版本。

我们可以通过将第一行更改为以下内容来解决此问题:

FROM mcr.microsoft.com/dotnet/core/runtime:2.2-nanoserver-sac2016 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-sac2016 AS build
WORKDIR /src
COPY ["MikesBank/MikesBank.csproj", "MikesBank/"]
... etc ...

进行此更改后,您可以重新发布到Azure,并运行此新容器注册表的实例。

在本地运行Docker镜像

我们还能够在我们自己的Windows本地副本上运行Docker Registry。为此,请确保已安装Docker For WindowsAzure CLI,然后执行以下步骤:

  1. 将存储容器注册表的订阅的GUID告诉Azure CLI
  2. 登录到订阅。
  3. 登录到Azure容器注册表。
  4. 使用Docker “pull命令将镜像的副本下载到您的笔记本电脑/服务器。
  5. 运行此镜像的本地副本。
  6. 获取所有正在运行的容器的列表,以获取新容器的ID
  7. 运行Dockerinspect命令,找出需要在浏览器中打开的IP地址才能打开您的应用程序。

因此,总体而言,这些命令如下所示(显然,您需要指定Azure为您自己的注册表副本创建的名称):

az account set --subscription <subscription_id>

az login

az acr login --name MikesBank20190925022604

docker pull mikesbank20190925022604.azurecr.io/mikesbank:latest

docker run mikesbank:latest

docker image ls

docker inspect -f "{{ .NetworkSettings.Networks.nat.IPAddress }}" <container_id>

最后,您将获得可以在浏览器中打开的IP地址,并查看Swagger页面。真好!

摘要

就是这样!

现在,我们有了一个不错的ASP.NET Core Web API项目,它具有友好的Swagger页面,SQL Server连接和日志记录。如果我们真的想在下一次工作面试中留下深刻的印象,并且能够告诉他们您的API支持“CRUDE” ,那么还可以加上导出到Excel”

发布了71 篇原创文章 · 获赞 152 · 访问量 52万+

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/105176454