• 用ASP.NET Core MVC 和 EF Core 构建Web应用 (一)


    系统必备

    • .NET Core 2.0.0 SDK 或更高版本。
    • 已安装 ASP.NET 和 Web 开发工作负载的 Visual Studio 2017 15.3 版或更高版本。

    创建Web应用程序

    打开 Visual Studio 并创建一个新 ASP.NET Core C# web 项目名为”ContosoUniversity”。

    • 文件菜单上,选择新建 > 项目

    • 从左窗格中,选择已安装 > Visual C# > Web

    • 选择“ASP.NET Core Web 应用程序”项目模板。

    • 输入ContosoUniversity作为名称,然后单击确定

    • 在 “新建 ASP.NET Core Web 应用程序” 对话框,选择ASP.NET Core 2.1和 Web 应用程序 (模型-视图-控制器)模板。

    • 注意:本教程需要安装 ASP.NET Core 2.0 和 EF Core 2.0 或更高版本。

    • 请确保身份验证设置为不进行身份验,单击“确定”。

     

    修改页面菜单布局和主页

    打开Views/Shared/_Layout.cshtml并进行以下更改:

    • 将文件中的”ContosoUniversity”更改为”Contoso University”。 需要更改三个地方。

    • 添加菜单项StudentsCoursesInstructors,和Department,并删除Contact菜单项。

    1 <li><a asp-area="" asp-controller="Students" asp-action="Index">Students</a></li>
    2 <li><a asp-area="" asp-controller="Courses" asp-action="Index">Courses</a></li>
    3 <li><a asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a></li>
    4 <li><a asp-area="" asp-controller="Departments" asp-action="Index">Departments</a></li>

    Views/Home/Index.cshtml,将文件的内容替换为以下代码以将有关 ASP.NET 和 MVC 的内容替换为有关此应用程序的内容:

    @{
        ViewData["Title"] = "Home Page";
    }
    
    <div class="jumbotron">
        <h1>Contoso University</h1>
    </div>
    <div class="row">
        <div class="col-md-4">
            <h2>Welcome to Contoso University</h2>
            <p>
                Contoso University is a sample application that
                demonstrates how to use Entity Framework Core in an
                ASP.NET Core MVC web application.
            </p>
        </div>
        <div class="col-md-4">
            <h2>Build it from scratch</h2>
            <p>You can build the application by following the steps in a series of tutorials.</p>
            <p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the tutorial &raquo;</a></p>
        </div>
        <div class="col-md-4">
            <h2>Download it</h2>
            <p>You can download the completed project from GitHub.</p>
            <p><a class="btn btn-default" href="https://github.com/aspnet/Docs/tree/master/aspnetcore/data/ef-mvc/intro/samples/cu-final">See project source code &raquo;</a></p>
        </div>
    </div>
    View Code

    CTRL + F5 来运行该项目或从菜单选择调试 > 开始执行不调试。 你会看到首页和将通过这个教程创建的页对应的选项卡。

    数据访问部分使用 EF Core

    若要为项目添加 EF Core 支持,需要安装相应的数据库驱动包。 本教程使用 SQL Server,相关驱动包Microsoft.EntityFrameworkCore.SqlServer。 该包包含在Microsoft.AspNetCore.All 包中,因此不需要手动安装。此包和其依赖项 (Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.Relational) 一起提供 EF 的运行时支持。 

    创建数据模型

    创建 Contoso 大学应用程序的实体类,从以下三个实体类开始。

    StudentEnrollment实体之间是一对多的关系,CourseEnrollment实体之间也是一个对多的关系。 换而言之,一名学生可以修读任意数量的课程, 并且某一课程可以被任意数量的学生修读。

    Models文件夹中,创建一个名为Student.cs的类文件并且将模板代码替换为以下代码。

     1 using System;
     2 using System.Collections.Generic;
     3 
     4 namespace ContosoUniversity.Models
     5 {
     6     public class Student
     7     {
     8         public int ID { get; set; }
     9         public string LastName { get; set; }
    10         public string FirstMidName { get; set; }
    11         public DateTime EnrollmentDate { get; set; }
    12 
    13         public ICollection<Enrollment> Enrollments { get; set; }
    14     }
    15 }

    ID属性将成为对应于此类的数据库表中的主键。 默认情况下,EF 将会将名为IDclassnameID的属性解析为主键。

    Enrollments属性是导航属性。 导航属性中包含与此实体相关的其他实体。 在这个案例下,Student entity中的Enrollments属性会保留所有与Student实体相关的Enrollment。 换而言之,如果在数据库中有两行描述同一个学生的修读情况 (两行的 StudentID 值相同,而且 StudentID 作为外键和某位学生的主键值相同),Student实体的Enrollments导航属性将包含那两个Enrollment实体。

    如果导航属性可以具有多个实体 (如多对多或一对多关系),那么导航属性的类型必须是可以添加、 删除和更新条目的容器,如ICollection<T>。 你可以指定ICollection<T>或实现该接口类型,如List<T>HashSet<T>。 如果指定ICollection<T>,EF在默认情况下创建HashSet<T>集合。

    Models文件夹中,创建Enrollment.cs并且用以下代码替换现有代码:

     1 namespace ContosoUniversity.Models
     2 {
     3     public enum Grade
     4     {
     5         A, B, C, D, F
     6     }
     7 
     8     public class Enrollment
     9     {
    10         public int EnrollmentID { get; set; }
    11         public int CourseID { get; set; }
    12         public int StudentID { get; set; }
    13         public Grade? Grade { get; set; }
    14 
    15         public Course Course { get; set; }
    16         public Student Student { get; set; }
    17     }
    18 }

    EnrollmentID属性将被设为主键; 此实体使用classnameID模式而不是如Student实体那样直接使用ID。 通常情况下,你选择一个主键模式,并在你的数据模型自始至终使用这种模式。 在这里,使用了两种不同的模式只是为了说明你可以使用任一模式来指定主键。 

    Grade属性是enum。 Grade声明类型后的?表示Grade属性可以为 null。 评级为 null 和评级为零是有区别的 –null 意味着评级未知或者尚未分配。

    StudentID属性是一个外键,Student是与其且对应的导航属性。 Enrollment实体与一个Student实体相关联,因此该属性只包含单个Student实体 (与前面所看到的Student.Enrollments导航属性不同后,Student中可以容纳多个Enrollment实体)。

    CourseID属性是一个外键,Course是与其对应的导航属性。 Enrollment实体与一个Course实体相关联。

    如果一个属性名为<导航属性名><主键属性名>,Entity Framework 就会将这个属性解析为外键属性(例如,Student实体的主键是IDStudentEnrollment的导航属性所以Enrollment实体中StudentID会被解析为外键)。 此外还可以将需要解析为外键的属性命名为<主键属性名>(例如,CourseID由于Course实体的主键所以CourseID也被解析为外键)。

    Models文件夹中,创建Course.cs并且用以下代码替换现有代码:

     1 using System.Collections.Generic;
     2 using System.ComponentModel.DataAnnotations.Schema;
     3 
     4 namespace ContosoUniversity.Models
     5 {
     6     public class Course
     7     {
     8         [DatabaseGenerated(DatabaseGeneratedOption.None)]
     9         public int CourseID { get; set; }
    10         public string Title { get; set; }
    11         public int Credits { get; set; }
    12 
    13         public ICollection<Enrollment> Enrollments { get; set; }
    14     }
    15 }

    Enrollments属性是导航属性。 一个Course实体可以与任意数量的Enrollment实体相关。

    创建数据库上下文

    使得给定的数据模型与 Entity Framework 功能相协调的主类是数据库上下文类。 可以通过继承 Microsoft.EntityFrameworkCore.DbContext 类的方式创建此类。 在该类中你可以指定数据模型中包含哪些实体。 你还可以定义某些 Entity Framework 行为。 在此项目中将数据库上下文类命名为SchoolContext

    在项目文件夹中,创建名为的文件夹Data。在Data文件夹创建名为SchoolContext.cs的类文件,并将模板代码替换为以下代码:

     1 using ContosoUniversity.Models;
     2 using Microsoft.EntityFrameworkCore;
     3 
     4 namespace ContosoUniversity.Data
     5 {
     6     public class SchoolContext : DbContext
     7     {
     8         public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
     9         {
    10         }
    11 
    12         public DbSet<Course> Courses { get; set; }
    13         public DbSet<Enrollment> Enrollments { get; set; }
    14         public DbSet<Student> Students { get; set; }
    15     }
    16 }
    View Code

    此代码将为每个实体集创建DbSet属性。 在 Entity Framework 中,实体集通常与数据表相对应,具体实体与表中的行相对应。

    在这里可以省略DbSet<Enrollment>DbSet<Course>语句,实现的功能没有任何改变。 Entity Framework 会隐式包含这两个实体因为Student实体引用了Enrollment实体、Enrollment实体引用了Course实体。

    当数据库创建完成后, EF 创建一系列数据表,表名默认和DbSet属性名相同。 集合属性的名称一般使用复数形式,但不同的开发人员的命名习惯可能不一样,开发人员根据自己的情况确定是否使用复数形式。在最后一个 DbSet 属性之后添加以下代码,对 DbContext 指定单数的表明来覆盖默认的表名。

    1         protected override void OnModelCreating(ModelBuilder modelBuilder)
    2         {
    3             modelBuilder.Entity<Course>().ToTable("Course");
    4             modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
    5             modelBuilder.Entity<Student>().ToTable("Student");
    6         }
    View Code

    用依赖注入注册上下文

    ASP.NET Core 默认实现依赖注入。在应用程序启动过程通过依赖注入注册相关服务 (例如 EF 数据库上下文)。 需要这些服务的组件 (如 MVC 控制器) 可以通过向构造函数添加相关参数来获得对应服务。

    若要将SchoolContext注册为一种服务,打开Startup.cs,并将以下代码添加到ConfigureServices方法中。

    1 public void ConfigureServices(IServiceCollection services)
    2 {
    3     services.AddDbContext<SchoolContext>(options =>
    4         options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    5 
    6     services.AddMvc();
    7 }
    View Code

    通过调用DbContextOptionsBuilder中的一个方法将数据库连接字符串在配置文件中的名称传递给上下文对象。 进行本地开发时, ASP.NET Core 配置系统appsettings.json文件中读取数据库连接字符串。

    添加using语句引用ContosoUniversity.DataMicrosoft.EntityFrameworkCore命名空间,然后生成项目。

    打开appsettings.json文件并添加连接字符串,如下所示。

     1 {
     2   "ConnectionStrings": {
     3     "DefaultConnection": "Server=(localdb)\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true"
     4   },
     5   "Logging": {
     6     "IncludeScopes": false,
     7     "LogLevel": {
     8       "Default": "Warning"
     9     }
    10   }
    11 }

    数据库连接字符串指定使用 SQL Server LocalDB 数据库。 LocalDB 是 SQL Server Express 数据库引擎的轻量级版本,用于应用程序开发,不在生产环境中使用。 LocalDB 作为按需启动并在用户模式下运行的轻量级数据库没有复杂的配置。 默认情况下, LocalDB 在C:/Users/<user>目录下创建.mdf数据库文件。

    用测试数据初始化数据库

    Entity Framework 已经为你创建了一个空数据库。在本部分中编写一个方法用于向数据库填充测试数据,该方法会在数据库创建完成之后执行。

    此处将使用EnsureCreated方法来自动创建数据库。 在Data文件夹中,创建名为的新类文件DbInitializer.cs并且将模板代码替换为以下代码,使得在需要时能创建数据库并向其填充测试数据。

     1 using ContosoUniversity.Models;
     2 using System;
     3 using System.Linq;
     4 
     5 namespace ContosoUniversity.Data
     6 {
     7     public static class DbInitializer
     8     {
     9         public static void Initialize(SchoolContext context)
    10         {
    11             context.Database.EnsureCreated();
    12 
    13             // Look for any students.
    14             if (context.Students.Any())
    15             {
    16                 return;   // DB has been seeded
    17             }
    18 
    19             var students = new Student[]
    20             {
    21             new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")},
    22             new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")},
    23             new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")},
    24             new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")},
    25             new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
    26             new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")},
    27             new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")},
    28             new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")}
    29             };
    30             foreach (Student s in students)
    31             {
    32                 context.Students.Add(s);
    33             }
    34             context.SaveChanges();
    35 
    36             var courses = new Course[]
    37             {
    38             new Course{CourseID=1050,Title="Chemistry",Credits=3},
    39             new Course{CourseID=4022,Title="Microeconomics",Credits=3},
    40             new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
    41             new Course{CourseID=1045,Title="Calculus",Credits=4},
    42             new Course{CourseID=3141,Title="Trigonometry",Credits=4},
    43             new Course{CourseID=2021,Title="Composition",Credits=3},
    44             new Course{CourseID=2042,Title="Literature",Credits=4}
    45             };
    46             foreach (Course c in courses)
    47             {
    48                 context.Courses.Add(c);
    49             }
    50             context.SaveChanges();
    51 
    52             var enrollments = new Enrollment[]
    53             {
    54             new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
    55             new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
    56             new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
    57             new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
    58             new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
    59             new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
    60             new Enrollment{StudentID=3,CourseID=1050},
    61             new Enrollment{StudentID=4,CourseID=1050},
    62             new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
    63             new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
    64             new Enrollment{StudentID=6,CourseID=1045},
    65             new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
    66             };
    67             foreach (Enrollment e in enrollments)
    68             {
    69                 context.Enrollments.Add(e);
    70             }
    71             context.SaveChanges();
    72         }
    73     }
    74 }
    View Code

    这段代码首先检查是否有学生数据在数据库中,如果没有的话,就可以假定数据库是新建的,然后使用测试数据进行填充。代码中使用数组存放测试数据而不是使用List<T>集合是为了优化性能。

    Program.cs,修改Main方法,使得在应用程序启动时能执行以下操作:

    • 从依赖注入容器中获取数据库上下文实例。
    • 调用 seed 方法,将上下文传递给它。
    • Seed 方法完成此操作时释放上下文。
     1 using ContosoUniversity.Data;
     2 using Microsoft.AspNetCore;
     3 using Microsoft.AspNetCore.Hosting;
     4 using Microsoft.Extensions.DependencyInjection;
     5 using Microsoft.Extensions.Logging;
     6 using System;
     7 
     8 namespace ContosoUniversity
     9 {
    10     public class Program
    11     {
    12         public static void Main(string[] args)
    13         {
    14             var host = CreateWebHostBuilder(args).Build();
    15             using (var scope = host.Services.CreateScope())
    16             {
    17                 var services = scope.ServiceProvider;
    18                 try
    19                 {
    20                     var context = services.GetRequiredService<SchoolContext>();
    21                     DbInitializer.Initialize(context);
    22                 }
    23                 catch (Exception ex)
    24                 {
    25                     var logger = services.GetRequiredService<ILogger<Program>>();
    26                     logger.LogError(ex, "An error occurred while seeding the database.");
    27                 }
    28             }
    29             host.Run();
    30         }
    31 
    32         public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    33             WebHost.CreateDefaultBuilder(args)
    34                 .UseStartup<Startup>();
    35     }
    36 }
    View Code

    现在首次运行该应用程序,创建数据库并使用测试数据作为种子数据。 每当你更改你的数据模型,你可以删除数据库、 更新你的 Initialize 方法,然后使用上述方式更新新数据库。 在之后的教程中,你将了解如何在数据模型更改时,只需修改数据库而无需删除重建数据库。

    创建控制器和视图

    用 Visual Studio 中的基架引擎添加一个 MVC 控制器和使用 EF 来查询和保存数据的视图。

    CRUD 操作方法和视图的自动创建被称为基架。 基架与代码生成不同,基架的代码是一个起点,您可以修改基架以满足自己需求,而你通常无需修改生成的代码。 当你需要自定义生成代码时,你使用一部分类或需求发生变化时重新生成代码。

    • 右键单击解决方案资源管理器中的Controllers文件夹选择添加 > 新搭建的基架项目
    • 在添加基架的对话框中:

      • 选择视图使用 Entity Framework 的 MVC 控制器

      • 单击添加

    • 在添加控制器对话框中:

      • 在模型类选择Student

      • 在数据上下文类选择SchoolContext

      • 使用StudentsController作为默认名字

      • 单击添加

     

    • 当你单击添加后,Visual Studio 基架引擎创建StudentsController.cs文件和一组对应于控制器的视图 (.cshtml文件) 。

    (如果你之前手动创建数据库上下文,基架引擎还可以自动创建。 你可以在添加控制器对话框中单击右侧的加号框数据上下文类来指定在一个新上下文类。然后,Visual Studio 将创建你的DbContext,控制器和视图类。)

    你会注意到控制器采用SchoolContext作为构造函数参数。

        public class StudentsController : Controller
        {
            private readonly SchoolContext _context;
    
            public StudentsController(SchoolContext context)
            {
                _context = context;
            }

    ASP.NET 依赖注入机制将会处理传递一个SchoolContext实例到控制器。

    控制器包含Index操作方法用于显示数据库中的所有学生。 该方法从学生实体集中获取学生列表,学生实体集则是通过读取数据库上下文实例中的Students属性获得:

    public async Task<IActionResult> Index()
    {
        return View(await _context.Students.ToListAsync());
    }

    Views/Students/Index.cshtml视图使用table标签显示此列表:

     1 @model IEnumerable<ContosoUniversity.Models.Student>
     2 
     3 @{
     4     ViewData["Title"] = "Index";
     5 }
     6 
     7 <h2>Index</h2>
     8 
     9 <p>
    10     <a asp-action="Create">Create New</a>
    11 </p>
    12 <table class="table">
    13     <thead>
    14         <tr>
    15                 <th>
    16                     @Html.DisplayNameFor(model => model.LastName)
    17                 </th>
    18                 <th>
    19                     @Html.DisplayNameFor(model => model.FirstMidName)
    20                 </th>
    21                 <th>
    22                     @Html.DisplayNameFor(model => model.EnrollmentDate)
    23                 </th>
    24             <th></th>
    25         </tr>
    26     </thead>
    27     <tbody>
    28 @foreach (var item in Model) {
    29         <tr>
    30             <td>
    31                 @Html.DisplayFor(modelItem => item.LastName)
    32             </td>
    33             <td>
    34                 @Html.DisplayFor(modelItem => item.FirstMidName)
    35             </td>
    36             <td>
    37                 @Html.DisplayFor(modelItem => item.EnrollmentDate)
    38             </td>
    39             <td>
    40                 <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
    41                 <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
    42                 <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
    43             </td>
    44         </tr>
    45 }
    46     </tbody>
    47 </table>
    View Code

    按 CTRL + F5 来运行该项目或从菜单选择调试 > 开始执行(不调试)。

    单击学生选项卡以查看DbInitializer.Initialize插入的测试的数据。 你将看到Student选项卡链接在页的顶部或在单击右上角后的导航图标中,具体显示在哪里取决于浏览器窗口宽度。

        

    约定

    由于 Entity Framwork 有一定的约束条件,你只需要按规则编写很少的代码就能够创建一个完整的数据库,

    • DbSet类型的属性用作表名。 实体未被DbSet属性引用,实体类名称用作表名称。

    • 实体属性名称用于列名称。

    • ID 或 classnameID 命名的实体属性被识别为主键属性。

    • 如果属性名为 <导航属性名> <主键名>将被解释为外键属性 (例如,StudentID对应Student导航属性,Student实体的主键是ID,所以StudentID被解释为外键属性). 此外也可以将外键属性命名为 <主键属性名> (例如,EnrollmentID,由于Enrollment实体的主键是EnrollmentID,因此被解释为外键)。

    约定行为可以被重写。 例如,设置列名称和将任何属性设置为主键或外键。

    异步代码

    异步编程是 ASP.NET Core 和 EF Core 的默认模式。

    Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行任何操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,异步代码使得服务器更有效地使用资源,并且该服务器可以无延迟地处理更多流量。

    异步代码在运行时,会引入的少量开销,在低流量时对性能的影响可以忽略不计,但在针对高流量情况下潜在的性能提升是可观的。

    在下面的代码中,async关键字,Task<T>返回值,await关键字,和ToListAsync方法使代码异步执行。

    public async Task<IActionResult> Index()
    {
        return View(await _context.Students.ToListAsync());
    }
    • async关键字用于告知编译器该方法主体将生成回调并自动创建Task<IActionResult>返回对象。

    • 返回类型Task<IActionResult>表示正在进行的工作返回的结果为IActionResult类型。

    • await关键字会使得编译器将方法拆分为两个部分。 第一部分是以异步方式结束已启动的操作。 第二部分是当操作完成时注入调用回调方法的地方。

    • ToListAsync是由ToList方法的的异步扩展版本。

    你使用 Entity Framework 编写异步代码时的一些注意事项:

    • 只有导致查询或发送数据库命令的语句才能以异步方式执行。 包括 ToListAsync, SingleOrDefaultAsync,和SaveChangesAsync。 不包括,操作IQueryable的语句,如var students = context.Students.Where(s => s.LastName == "Lilo")

    • EF 上下文是线程不安全的: 请勿尝试并行执行多个操作。 当调用异步 EF 方法时,始终使用await关键字。

    • 如果你想要利用异步代码的性能优势,请确保你所使用的任何库和包在它们调用导致 Entity Framework 数据库查询方法时也使用异步。 

    总结

    现已创建了一个使用 Entity Framework Core 和 SQL Server Express LocalDB 来存储和显示数据的简单应用程序。 在下一篇中,将介绍如何执行基本的 CRUD (创建、 读取、 更新、 删除) 操作。

     *****************************
     *** Keep learning and growing. ***
     *****************************
  • 相关阅读:
    spring-boot:run启动时,如何带设置环境参数dev,test.
    git多人参与的项目 -> 分支代码如何合并到主干
    如何使用IDEA运行 一个分布式的项目
    学习反射 并尝试写一个反射的工具类
    SVN提交大量无效文件补救方法
    IDEA基础配置
    Eclipse常用快捷键与IDEA中的对比.
    如何运行一个分布式的Maven项目
    Java常考面试题整理(六)
    python并发——多进程中的异常捕获
  • 原文地址:https://www.cnblogs.com/gangle/p/9190287.html
Copyright © 2020-2023  润新知