首页 >> 大全

Web API 2 入门——使用ASP.NET Web API和Angular

2023-09-03 大全 30 作者:考证青年

由网络营

下载网络营训练包

在传统的Web应用程序中,客户机(浏览器)通过请求页面启动与服务器的通信。然后,服务器处理请求,并将页面的HTML发送给客户端。在与页面的后续交互中,用户导航到链接或提交带有数据的表单 - 新的请求将发送到服务器,流程将再次启动:服务器处理请求并将新页面发送到浏览器回应客户要求的新动作。

在单页面应用程序(SPA)中,整个页面在初始请求之后加载到浏览器中,但后续的交互通过Ajax请求进行。这意味着浏览器必须仅更新已更改的页面部分;没有必要重新加载整个页面。SPA方法减少了应用程序响应用户操作所花费的时间,从而导致更流畅的体验。

SPA的架构涉及传统Web应用程序中不存在的某些挑战。然而,像ASP.NET Web API这样的新兴技术,像这样的框架和CSS3提供的新的样式功能使得设计和构建SPA变得非常简单。

在这个动手实验室,您将利用这些技术来实施基于SPA概念的琐事网站Geek Quiz。您将首先使用ASP.NET Web API实现服务层,以公开所需的端点,以检索测验问题并存储答案。然后,您将使用和CSS3转换效果构建一个丰富和响应的UI。

所有示例代码和代码段都包含在Web Camps Kit中,可从获取。

概观

目标

在这个动手实验中,您将学习如何:

先决条件

完成此动手实验室需要以下内容:

建立

为了在这个动手实验室中运行练习,你需要首先设置你的环境。

打开资源管理器并浏览实验室的“源”文件夹。右键单击Setup.cmd,然后选择以管理员身份运行,启动安装过程,该过程将配置您的环境并安装本实验的 代码片段。如果显示“用户帐户控制”对话框,请确认该操作继续。

注意

在运行安装程序之前,请确保已经检查了该实验室的所有依赖项。

使用代码片段

在整个实验室文档中,将指示您插入代码块。为方便起见,这些代码大部分作为 代码片段提供,您可以从 2013中访问,以避免手动添加。

注意

每个练习都附有一个起始解决方案,位于练习的Begin文件夹中,可以让您独立于其他练习进行每项练习。请注意,在这些开始的解决方案中缺少在练习期间添加的代码段,并且在完成练习之前可能无法正常工作。在练习的源代码中,您还将找到一个包含 解决方案的End文件夹,其中包含完成相应练习中的步骤所产生的代码。您可以使用这些解决方案作为指导,如果您需要额外的帮助,当您通过这个动手实验室。

演习

这个动手实验室包括以下练习:

创建Web API创建SPA界面

预计完成本实验的时间:60分钟

注意

当您第一次启动 时,您必须选择一个预定义的设置集合。每个预定义的集合都被设计为匹配特定的开发风格,并确定窗口布局,编辑器行为,智能感知代码片段和对话框选项。本实验中的过程描述了在使用“常规开发设置”集合时在 中完成给定任务所需的操作。如果您为开发环境选择不同的设置集合,那么您应该考虑的步骤可能会有所不同。

练习1:创建Web API

SPA的关键部分之一是服务层。它负责处理由UI发送的Ajax调用,并响应于该调用返回数据。检索的数据应以机器可读格式呈现,以便客户端进行解析和使用。

Web API框架是ASP.NET堆栈的一部分,旨在使HTTP服务变得容易,通常通过 API发送和接收JSON或XML格式的数据。在本练习中,您将创建Web站点来托管Geek Quiz应用程序,然后实施后端服务,以使用ASP.NET Web API公开并保留测验数据。

任务1 - 创建Geek测验的初始项目

在此任务中,您将开始创建一个新的ASP.NET MVC项目,并支持基于 附带的一个ASP.NET项目类型的ASP.NET Web API。一个ASP.NET统一了所有的ASP.NET技术,并提供了根据需要进行混合和匹配的选项。然后,您将添加实体框架的模型类和数据库初始化器以插入测验问题。

打开 2013 for Web,然后选择File |新项目...开始一个新的解决方案。

创建新项目

创建新项目

在“新建项目”对话框中,在 C#|下选择ASP.NET Web应用程序网页标签。确保选择了.NET 4.5,将其命名为,选择位置,然后单击确定。

创建一个新的ASP.NET Web应用程序项目

创建一个新的ASP.NET Web应用程序项目

在“新建ASP.NET项目”对话框中,选择MVC模板并选择Web API选项。此外,请确保“身份验证”选项设置为“个人用户帐户”。单击确定继续。

使用MVC模板创建一个新项目,包括Web API组件

使用MVC模板创建一个新项目,包括Web API组件

在解决方案资源管理器中,右键单击项目的文件夹,然后选择Add |现有项...。

添加现有项目

添加现有项目

在“添加现有项目”对话框中,导航到“源/资产/模型”文件夹,然后选择所有文件。单击添加。

添加模型资产

添加模型资产

注意

通过添加这些文件,您将添加数据模型, 的数据库上下文和Geek Quiz应用程序的数据库初始化程序。

实体框架(EF)是一种对象关系映射器(ORM),它使您能够通过使用概念应用程序模型编程创建数据访问应用程序,而不是直接使用关系存储架构进行编程。您可以在这里了解有关实体框架的更多信息。

以下是刚刚添加的类的描述:

打开.asax.cs文件并添加以下using语句。

C#复制

using GeekQuiz.Models;

在方法的开头添加以下代码,将lizer设置为数据库初始化程序。

C#复制

public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { System.Data.Entity.Database.SetInitializer(new TriviaDatabaseInitializer()); AreaRegistration.RegisterAllAreas(); GlobalConfiguration.Configure(WebApiConfig.Register); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } } 

修改Home控制器以限制对经过身份验证的用户的访问。为此,请打开文件夹中的.cs文件,并将属性添加到类定义。

C#复制

namespace GeekQuiz.Controllers
{[Authorize]public class HomeController : Controller { public ActionResult Index() { return View(); } ... } } 

注意

该授权过滤器检查,看是否该用户进行身份验证。如果用户未通过身份验证,则返回HTTP状态码401(未经授权),而不调用该操作。您可以在全局,控制器级别或单个操作级别应用过滤器。

您现在将自定义网页的布局和品牌。为此,请在Views |中打开.文件通过使用Geek Quiz替换My ASP.NET应用程序来共享文件夹并更新元素的内容。

复制

<head><meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - Geek Quiztitle> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") head> 

在同一个文件中,通过删除关于和联系人链接并将主页链接重命名为播放来更新导航栏。另外,重命名应用程序名称链接到Geek Quiz。导航栏的HTML应如下所示。

复制

class="navbar navbar-inverse navbar-fixed-top">
class="container">
class="navbar-header"> @Html.ActionLink("Geek Quiz", "Index", "Home", null, new { @class = "navbar-brand" })
class="navbar-collapse collapse">
    class="nav navbar-nav">
  • @Html.ActionLink("Play", "Index", "Home")
@Html.Partial("_LoginPartial")

通过使用Geek Quiz替换“我的ASP.NET应用程序”来更新布局页面的页脚。为此,请使用以下突出显示的代码替换元素的内容。

HTML复制

<div class="container body-content">@RenderBody()<hr /> <footer> <p>© @DateTime.Now.Year - Geek Quizp> footer> div> 

任务2 - 创建 Web API

在上一个任务中,您创建了Geek Quiz Web应用程序的初始结构。您现在将构建一个简单的Web API服务,它与测验数据模型进行交互,并公开以下操作:

您将使用 提供的ASP.NET脚手架工具为Web API控制器类创建基准。

打开文件夹中的.cs文件。该文件定义了Web API服务的配置,就像路由映射到Web API控制器操作一样。

在文件开头添加以下using语句。

C#复制

using Newtonsoft.Json.Serialization;

将以下突出显示的代码添加到方法中,以全局配置Web API操作方法检索的JSON数据的格式化程序。

C#复制

public static class WebApiConfig
{public static void Register(HttpConfiguration config) { // Web API configuration and services // Use camel case for JSON data. config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); // Web API routes config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } 

注意

该自动属性名称转换为骆驼的情况下,这是在属性名称的一般惯例。

在解决方案资源管理器中,右键单击项目的文件夹,然后选择Add |新搭建的项目......。

创建一个新的脚手架物品

创建一个新的脚手架物品

在“添加脚手架”对话框中,确保在左侧窗格中选择了“公共”节点。然后,在中央窗格中选择Web API 2控制器 - 空模板,然后单击添加。

选择Web API 2控制器空模板

选择Web API 2控制器空模板

注意

ASP.NET脚手架是ASP.NET Web应用程序的代码生成框架。 2013包括用于MVC和Web API项目的预安装代码生成器。当您希望快速添加与数据模型交互的代码,以减少开发标准数据操作所需的时间,您应该在项目中使用脚手架。

脚手架过程还确保所有所需的依赖关系都安装在项目中。例如,如果您从一个空的ASP.NET项目开始,然后使用脚手架来添加一个Web API控制器,则所需的Web API NuGet软件包和引用将自动添加到您的项目中。

在“添加控制器”对话框中,在“控制器名称”文本框中键入,然后单击“添加”。

添加Trivia控制器

添加控制器

然后将.cs文件添加到项目的文件夹中,其中包含一个空的类。在文件的开头添加以下使用语句。

(代码片段 - - Ex1 - gs)

C#复制

using System.Data.Entity;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Description;
using GeekQuiz.Models; 

在类的开头添加以下代码,以便在控制器中定义,初始化和处理实例。

(代码片段 - - Ex1 - ext)

C#复制

public class TriviaController : ApiController
{private TriviaContext db = new TriviaContext(); protected override void Dispose(bool disposing) { if (disposing) { this.db.Dispose(); } base.Dispose(disposing); } } 

注意

的处置的方法调用处置所述的方法例如,这确保了在由所述上下文对象中使用的所有资源被释放实例设置或垃圾收集。这包括关闭 打开的所有数据库连接。

在类的末尾添加以下帮助方法。此方法从数据库中检索以下问题以由指定的用户回答。

(代码片段 - - Ex1 - )

C#复制

private async Task NextQuestionAsync(string userId) { var lastQuestionId = await this.db.TriviaAnswers .Where(a => a.UserId == userId) .GroupBy(a => a.QuestionId) .Select(g => new { QuestionId = g.Key, Count = g.Count() }) .OrderByDescending(q => new { q.Count, QuestionId = q.QuestionId }) .Select(q => q.QuestionId) .FirstOrDefaultAsync(); var questionsCount = await this.db.TriviaQuestions.CountAsync(); var nextQuestionId = (lastQuestionId % questionsCount) + 1; return await this.db.TriviaQuestions.FindAsync(CancellationToken.None, nextQuestionId); } 

将以下Get操作方法添加到类中。此操作方法调用上一步中定义的帮助器方法来检索已验证用户的下一个问题。

(代码片段 - - Ex1 - ction)

C#复制

// GET api/Trivia
[ResponseType(typeof(TriviaQuestion))]
public async Task Get() { var userId = User.Identity.Name; TriviaQuestion nextQuestion = await this.NextQuestionAsync(userId); if (nextQuestion == null) { return this.NotFound(); } return this.Ok(nextQuestion); } 

在类的末尾添加以下帮助方法。该方法将指定的答案存储在数据库中,并返回一个布尔值,指示答案是否正确。

(代码片段 - - Ex1 - )

C#复制

private async Task<bool> StoreAsync(TriviaAnswer answer) { this.db.TriviaAnswers.Add(answer); await this.db.SaveChangesAsync(); var selectedOption = await this.db.TriviaOptions.FirstOrDefaultAsync(o => o.Id == answer.OptionId && o.QuestionId == answer.QuestionId); return selectedOption.IsCorrect; } 

将以下Post操作方法添加到类中。此操作方法将答案与经过身份验证的用户相关联,并调用帮助程序方法。然后,它发送一个响应与由辅助方法返回的布尔值。

(代码段 - - Ex1 - )

C#复制

// POST api/Trivia
[ResponseType(typeof(TriviaAnswer))]
public async Task Post(TriviaAnswer answer) { if (!ModelState.IsValid) { return this.BadRequest(this.ModelState); } answer.UserId = User.Identity.Name; var isCorrect = await this.StoreAsync(answer); return this.Ok<bool>(isCorrect); } 

修改Web API控制器以通过将授权属性添加到类定义来限制对经过身份验证的用户的访问。

C#复制

[Authorize]
public class TriviaController : ApiController { ... } 

任务3 - 运行解决方案

在此任务中,您将验证您在前一任务中构建的Web API服务是否按预期工作。您将使用 开发人员工具捕获网络流量并检查Web API服务的完整响应。

注意

确保在 工具栏上的“开始”按钮中选择了 。

按F5运行解决方案。将在登录页面应该出现在浏览器中。

注意

当应用程序启动时,默认的MVC路由被触发,默认情况下映射到类的Index操作。由于仅限于经过身份验证的用户(请记住,您使用练习1中的属性来修饰该类),并且还没有用户认证,应用程序将原始请求重定向到登录页面。

运行解决方案

运行解决方案

单击注册以创建新用户。

注册新用户

在注册页面中,输入用户名和密码,然后单击注册。

注册页面

注册页面

应用程序注册新帐户,用户被认证并重新定向到主页。

用户进行身份验证

用户进行身份验证

在浏览器中,按F12打开“开发人员工具”面板。按CTRL + 4或单击网络图标,然后单击绿色箭头按钮开始捕获网络流量。

启动Web API网络捕获

启动Web API网络捕获

在浏览器地址栏中的URL附加api / 。您现在将从中的Get操作方法检查响应的详细信息。

通过Web API检索下一个问题数据

通过Web API检索下一个问题数据

注意

一旦下载完成,系统将提示您对下载的文件进行操作。让对话框打开,以便能够通过开发人员工具窗口来观看响应内容。

现在你将检查身体的反应。为此,请单击详细信息选项卡,然后单击响应正文。您可以检查下载的数据是否与具有对应于类的属性选项(它是对象的列表),id和标题的对象。

查看Web API响应体

查看Web API响应体

返回到 ,然后按SHIFT + F5停止调试。

练习2:创建SPA界面

在本练习中,您将首先构建Geek Quiz的Web前端部分,重点介绍使用的单页应用程序交互。然后,您将通过CSS3增强用户体验,以执行丰富的动画,并在从一个问题转换到下一个问题时提供上下文切换的视觉效果。

任务1 - 使用创建SPA接口

在这个任务中,您将使用来实现Geek Quiz应用程序的客户端。是一个开放源码的框架,通过模型 - 视图 - 控制器(MVC)功能来增强基于浏览器的应用程序,有助于开发和测试。

您将首先从 的软件包管理器控制台安装。然后,您将创建控制器以提供Geek Quiz应用程序的行为和使用模板引擎呈现测验问题和答案的视图。

注意

有关的更多信息,请参阅[]())。

打开 2013 for Web,并打开位于 / Ex2-e / Begin文件夹中的.sln解决方案。或者,您可以继续在上一个练习中获得的解决方案。

从工具|打开包管理器控制台图书馆包裹经理。键入以下命令来安装.包。

_入门使用综合效果器_入门使用复合弓还是反曲弓合适

电源外壳复制

Install-Package AngularJS.Core

在解决方案资源管理器中,右键单击项目的脚本文件夹,然后选择添加新建文件夹。命名文件夹应用程序,然后按Enter键。

右键单击刚刚创建的应用程序文件夹,然后选择添加|文件。

创建一个新的JavaScript文件

创建一个新的文件

在“指定项目名称”对话框中,在“项目名称”文本框中键入quiz-,然后单击“确定”。

命名新的JavaScript文件

命名新的文件

在quiz-.js文件中,添加以下代码来声明和初始化控制器。

(代码片段 - - Ex2 - r)

的复制

angular.module('QuizApp', []).controller('QuizCtrl', function ($scope, $http) { $scope.answered = false; $scope.title = "loading question..."; $scope.options = []; $scope.correctAnswer = false; $scope.working = false; $scope.answer = function () { return $scope.correctAnswer ? 'correct' : 'incorrect'; }; }); 

注意

控制器的构造函数需要一个名为$ scope的可注射参数。应在构造函数中通过将属性附加到$ scope对象来设置范围的初始状态。属性包含视图模型,并且在注册控制器时可以访问模板。

所述控制器被命名模块中定义。模块是工作单元,可以将您的应用程序分解成单独的组件。使用模块的主要优点是代码更容易理解,便于单元测试,可重用性和可维护性。

您现在将向作用域添加行为,以便对从视图触发的事件作出反应。在控件的末尾添加以下代码,以定义$ scope对象中的函数。

(代码片段 - - Ex2 - )

的复制

.controller('QuizCtrl', function ($scope, $http) { ...$scope.nextQuestion = function () { $scope.working = true; $scope.answered = false; $scope.title = "loading question..."; $scope.options = []; $http.get("/api/trivia").success(function (data, status, headers, config) { $scope.options = data.options; $scope.title = data.title; $scope.answered = false; $scope.working = false; }).error(function (data, status, headers, config) { $scope.title = "Oops... something went wrong"; $scope.working = false; }); }; }; 

注意

此函数从上一个练习中创建的 API中检索下一个问题,并将问题数据附加到$ scope对象。

在控件的末尾插入以下代码,以定义$ scope对象中的函数。

(代码片段 - - Ex2 - )

的复制

.controller('QuizCtrl', function ($scope, $http) { ...$scope.sendAnswer = function (option) { $scope.working = true; $scope.answered = true; $http.post('/api/trivia', { 'questionId': option.questionId, 'optionId': option.id }).success(function (data, status, headers, config) { $scope.correctAnswer = (data === true); $scope.working = false; }).error(function (data, status, headers, config) { $scope.title = "Oops... something went wrong"; $scope.working = false; }); }; }; 

注意

此功能将用户选择的答案发送到 API,并存储结果,如果答案是否正确,则在$ scope对象中。

上述的和函数使用$ http对象通过浏览器中的 对象抽象与Web API的通信。支持另一种服务,它通过 API为资源执行CRUD操作提供了更高的抽象级别。$资源对象具有提供高级行为的操作方法,而不需要与$ http对象进行交互。考虑在需要CRUD模型的场景中使用$资源对象(前提信息,请参阅$资源文档)。

下一步是创建定义测验视图的模板。为此,请在Views |中打开Index.文件主文件夹,并用以下代码替换内容。

(Code - - Ex2 - )

复制

@{ViewBag.Title = "Play";
}<div id="bodyContainer" ng-app="QuizApp"> 
id="content"> <div class="container" > <div class="row"> <div class="flip-container text-center col-md-12" ng-controller="QuizCtrl" ng-init="nextQuestion()"> <div class="back" ng-class="{flip: answered, correct: correctAnswer, incorrect:!correctAnswer}">

class="lead">{{answer()}}

div> <div class="front" ng-class="{flip: answered}">

class="lead">{{title}}

<div class="row text-center"> div> div> div> div> div>
div> @section scripts { @Scripts.Render("~/Scripts/angular.js") @Scripts.Render("~/Scripts/app/quiz-controller.js") }

注意

模板是一个声明性规范,它使用模型信息和控制器将静态标记转换为用户在浏览器中看到的动态视图。以下是可以在模板中使用的元素和元素属性的示例:

打开文件夹中的Site.css文件,并在文件末尾添加以下突出显示的样式,以提供测验视图的外观。

(代码片段 - - Ex2 - )

CSS复制

.validation-summary-valid {display: none;
}/* Geek Quiz styles */
.flip-container .back, .flip-container .front { border: 5px solid #00bcf2; padding-bottom: 30px; padding-top: 30px; } #content { position:relative; background:#fff; padding:50px 0 0 0; } .option { width:140px; margin: 5px; } div.correct p { color: green; } div.incorrect p { color: red; } .btn { border-radius: 0; } .flip-container div.front, .flip-container div.back.flip { display: block; } .flip-container div.front.flip, .flip-container div.back { display: none; } 

任务2 - 运行解决方案

在此任务中,您将使用您使用构建的新用户界面来执行解决方案,以回答一些测验问题。

按F5运行解决方案。

注册一个新的用户帐号。为此,请按照练习1,任务3中所述的注册步骤进行操作。

注意

如果您使用上一个练习中的解决方案,您可以使用之前创建的用户帐户登录。

该主页页面应该出现,显示了测验的第一个问题。通过单击其中一个选项来回答问题。这将触发先前定义的函数,该函数将选定的选项发送到 API。

回答一个问题

回答一个问题

点击其中一个按钮后,应该会出现答案。单击下一个问题以显示以下问题。这将触发控制器中定义的函数。

请求下一个问题

请求下一个问题

下一个问题应该出现。继续多次回答问题。完成所有问题后,您应该回到第一个问题。

另一个问题

下一个问题

返回到 ,然后按SHIFT + F5停止调试。

任务3 - 使用CSS3创建翻转动画

在这个任务中,您将使用CSS3属性来执行丰富的动画,通过在问题回答和下一个问题被检索时添加翻转效果。

在解决方案资源管理器中,右键单击项目的“内容”文件夹,然后选择“添加”现有项...。

将现有项目添加到“内容”文件夹

将现有项目添加到“内容”文件夹

在“添加现有项目”对话框中,导航到“源/资源”文件夹,然后选择“Flip.css”。单击添加。

从资产添加Flip.css文件

从资产添加Flip.css文件

打开刚刚添加的Flip.css文件并检查其内容。

找到翻转变换注释。该评论下方的样式使用CSS透视图和转换来生成“卡片翻转”效果。

CSS复制

/* flip transformation */
.flip-container div.front {-moz-transform: perspective(2000px) rotateY(0deg); -webkit-transform: perspective(2000px) rotateY(0deg); -o-transform: perspective(2000px) rotateY(0deg); transform: perspective(2000px) rotateY(0deg); } .flip-container div.front.flip { -moz-transform: perspective(2000px) rotateY(179.9deg); -webkit-transform: perspective(2000px) rotateY(179.9deg); -o-transform: perspective(2000px) rotateY(179.9deg); transform: perspective(2000px) rotateY(179.9deg); } .flip-container div.back { -moz-transform: perspective(2000px) rotateY(-180deg); -webkit-transform: perspective(2000px) rotateY(-180deg); -o-transform: perspective(2000px) rotateY(-180deg); transform: perspective(2000px) rotateY(-180deg); } .flip-container div.back.flip { -moz-transform: perspective(2000px) rotateY(0deg); -webkit-transform: perspective(2000px) rotateY(0deg); -ms-transform: perspective(2000px) rotateY(0); -o-transform: perspective(2000px) rotateY(0); transform: perspective(2000px) rotateY(0); } 

在翻盖评论期间找到窗格的隐藏。通过将背景可见性CSS属性设置为隐藏,该注释下方的样式通过将背面可见性CSS属性设置为隐藏,从而隐藏了面部背面的背面。

CSS复制

/* hide back of pane during flip */
.front, .back {-moz-backface-visibility: hidden;-webkit-backface-visibility: hidden; backface-visibility: hidden; } 

打开文件夹中的.cs文件,并将引用添加到“〜/ / css”样式包中的Flip.css文件

C#复制

bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/bootstrap.css","~/Content/site.css","~/Content/Flip.css")); 

按F5运行解决方案并使用凭据登录。

通过点击其中一个选项来回答问题。注意在视图之间转换时的翻转效果。

回答一个具有翻转效果的问题

回答一个具有翻转效果的问题

单击下一个问题以检索以下问题。翻转效果应再次出现。

使用翻转效果检索以下问题

使用翻转效果检索以下问题

概要

通过完成这个动手实验,你已经学会了如何:

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了