当前位置: 首页 > news >正文

表示商业网站的域名高端医疗网站模板免费下载

表示商业网站的域名,高端医疗网站模板免费下载,wordpress 显示标签代码,湖南人文科技学院全国排名James: 《使用Blazor开发内部后台》系列是技术社区中一位朋友投稿的系列文章#xff0c;介绍自己为公司的 WebForm 遗留系统使用 Blazor 重写前端 UI 的经历。本文为第三篇#xff0c;如果错过了前两篇#xff0c;建议先阅读一下#xff1a;使用 Blazor 开发内部后台#… James: 《使用Blazor开发内部后台》系列是技术社区中一位朋友投稿的系列文章介绍自己为公司的 WebForm 遗留系统使用 Blazor 重写前端 UI 的经历。本文为第三篇如果错过了前两篇建议先阅读一下使用 Blazor 开发内部后台一认识Blazor使用 使用 Blazor 开发内部后台二了解 Blazor 组件前言前文为读者介绍了Blazor及组件的相关基础概念现在让我们来处理一些实际的问题。本文将介绍一个简单的设计方案如何基于Blazor开发内部后台登录页面及相关模块。为了方便初学者理解正文本文会先介绍一些工程上必须掌握的基础知识有经验的开发者可以选择性跳过。托管Blazor WA应用Hosted Blazor Web AssemblyBlazor WA应用可以单独部署称之为独立Blazor WAStandalone通常用于不需要后端的离线应用或者后端服务基于非ASP.NET Core的情形。而将Blazor作为ASP.NET Core应用的前端部分一起部署则被称为托管BlazorHosted。很显然若要开发一个前后端分离的应用采用托管Blazor才能最大程度地发挥Blazor的开发和部署优势。项目基本结构托管Blazor WA应用的项目解决方案主要包含三大子项目XXX.Client客户端项目前端模块即Blazor应用。XXX.Server服务端项目后端模块通常是ASP.NET Core Web API。在最后部署的时候是由此项目进行发布的因此该项目会引用Client项目。XXX.Shared类库项目共享模块主要是存放前后端可以共用的数据或逻辑其他2个项目都要引用它。而针对Client项目内部也有自己的默认结构这里请读者自行阅读Blazor项目结构官方文档篇幅所限后文将默认读者已经熟悉这些基础结构。依赖注入依赖注入是ASP.NET Core里一个非常基础的设计模式。Blazor里延续了和后端开发同样的风格。例如前端向后端发送请求需要使用HttpClient在Program.cs文件里可以看到 public class Program{public static async Task Main(string[] args){var builder WebAssemblyHostBuilder.CreateDefault(args);builder.RootComponents.AddApp(#app);builder.Services.AddScoped(sp new HttpClient{BaseAddress new Uri(builder.HostEnvironment.BaseAddress),Timeout TimeSpan.FromSeconds(3)});await builder.Build().RunAsync();}} 又例如我们按照Ant-Design-Blazor项目的《快速上手》说明引入该开源组件Nuget包后也需要在这里加上依赖注入的代码行其他需要的操作详见项目文档builder.Services.AddAntDesign(); 这对ASP.NET Core后端开发者来说完全没有理解门槛。而在Page文件里需要使用HttpClient时只需要使用inject关键词声明即可inject HttpClient MyHttpClientdiv....... /divcode{private async Taskstring GetAsync(){string rsp await MyHttpClient.GetStringAsync(xxxx);return rsp;} } 这里请读者自行阅读Blazor依赖注入的官方文档。对Angular开发者来说应该也会感到十分亲切。设计认证方式谈到登录自然最先要考虑登录的认证方式常见的有Cookie、Session或Token。对后端渲染的应用来说使用Session应该更简单而对前后端分离的应用来说后端Web API应当是无状态的因此一般只选择Cookie或Token由前端持有自己的身份票据后端做验证而不存储。而在Cookie和Token之间我按照官方文档的建议选择了使用Json Web Token。这里有必要将官方的理由引用过来方便读者参考还有对 SPA 进行身份验证的其他选项例如使用 SameSite cookie。但是Blazor WebAssembly 的工程设计决定OAuth 和 OIDC 是在 Blazor WebAssembly 应用中进行身份验证的最佳选择。出于以下功能和安全原因选择了以 JSON Web 令牌 (JWT) 为基础的基于令牌的身份验证而不是基于 cookie 的身份验证使用基于令牌的协议可以减小攻击面因为并非所有请求中都会发送令牌。服务器终结点不要求针对跨站点请求伪造 (CSRF) 进行保护因为会显式发送令牌。因此可以将 Blazor WebAssembly 应用与 MVC 或 Razor Pages 应用一起托管。令牌的权限比 cookie 窄。例如令牌不能用于管理用户帐户或更改用户密码除非显式实现了此类功能。令牌的生命周期更短默认为一小时这限制了攻击时间窗口。还可随时撤销令牌。自包含 JWT 向客户端和服务器提供身份验证进程保证。例如客户端可以检测和验证它收到的令牌是否合法以及是否是在给定身份验证过程中发出的。如果有第三方尝试在身份验证进程中偷换令牌客户端可以检测被偷换的令牌并避免使用它。OAuth 和 OIDC 的令牌不依赖于用户代理行为正确以确保应用安全。基于令牌的协议例如 OAuth 和 OIDC允许用同一组安全特征对托管和独立应用进行验证和授权。官方最推荐的方式是使用OAuth和OIDC。但开发内部后台还要另搞一个OAuth服务器对绝大多数开发者来说维护和部署成本过高了。所以我使用了传统的Password模式后端自生成JWT。对内部后台应用来说这么做已经足够安全。还需要考虑的问题是前端如何存放JWT呢我们仍有两种选择Cookie和LocalStorage。如果拿到了JWT放到一个前端自生成的Cookie里……那为什么不一开始就用Cookie呢显得有些自我矛盾。我选择了储存到LocalStorage里。借助开源项目Blazor.LocalStorage我们可以很轻松地达到目的当然跟Antd一样要用到依赖注入builder.Services.AddBlazoredLocalStorage(config {config.JsonSerializerOptions.DictionaryKeyPolicy JsonNamingPolicy.CamelCase;config.JsonSerializerOptions.IgnoreNullValues true;config.JsonSerializerOptions.IgnoreReadOnlyProperties true;config.JsonSerializerOptions.PropertyNameCaseInsensitive true;config.JsonSerializerOptions.PropertyNamingPolicy JsonNamingPolicy.CamelCase;config.JsonSerializerOptions.ReadCommentHandling JsonCommentHandling.Skip;config.JsonSerializerOptions.WriteIndented false;}); 设计后端接口既然已经确认要使用JWT那么后端自然要提供一个认证的接口 public class AccountController : ApiControllerBase{private readonly IMemoryCache _cache;private readonly IOptionsMonitorJwtOption _jwtOpt;private readonly IPasswordCryptor _passwordCryptor;private readonly MyDbContext _efContext;public AccountController(ILoggerAccountController logger,IMemoryCache cache,IOptionsMonitorJwtOption jwtOpt,IPasswordCryptor passwordCryptor,MyDbContext efContext) : base(logger){_cache cache;_jwtOpt jwtOpt;_passwordCryptor passwordCryptor;_efContext efContext;}[HttpPost]public async TaskIActionResult Login([FromForm] LoginRqtDto rqtDto){var cryptedPwd _passwordCryptor.Encrypt(rqtDto.Password, default);string adminIdCacheKey CacheKeyHelper.GetAdminIdCacheKey(rqtDto.Account);if (!_cache.TryGetValue(adminIdCacheKey, out int adminId)){adminId await _efContext.Admins.Where(a a.Account rqtDto.Account a.Password cryptedPwd).Select(a a.AdminId).FirstOrDefaultAsync();if (adminId 1){return Unauthorized();}_cache.Set(adminIdCacheKey, adminId, TimeSpan.FromDays(1));}else{bool checkPwd await _efContext.Admins.AnyAsync(a a.AdminId adminId a.Password cryptedPwd);if (!checkPwd){return Unauthorized();}}var claims new Claim[]{new(ClaimTypes.NameIdentifier, adminId.ToString()),new(ClaimTypes.Name, rqtDto.Account),new(ClaimTypes.Role, admin)};var jwtSetting _jwtOpt.CurrentValue;var key new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.Key));var creds new SigningCredentials(key, SecurityAlgorithms.HmacSha256);var expiry DateTime.Now.AddHours(jwtSetting.ExpiryInHours);var token new JwtSecurityToken(jwtSetting.Issuer, jwtSetting.Audience, claims, expires: expiry, signingCredentials: creds);var tokenText new JwtSecurityTokenHandler().WriteToken(token);return Ok(tokenText);}} 还需要配置JWT相关的参数 JWT: {Key: xxx,Issuer: xxx,Audience: xxx,ExpiryInHours: 8}及依赖注入 public static IServiceCollection AddAuth(this IServiceCollection services, IConfiguration configuration){services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options {options.TokenValidationParameters new TokenValidationParameters{ValidateIssuer true,ValidateAudience true,ValidateLifetime true,ValidateIssuerSigningKey true,ValidIssuer configuration.GetValuestring(JWT:Issuer),ValidAudience configuration.GetValuestring(JWT:Audience),IssuerSigningKey new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.GetValuestring(JWT:Key))),RequireExpirationTime true};});services.ConfigureJwtOption(configuration.GetSection(JWT));return services;} 以上代码仅供读者参考可按实际需要增删改。另有一句与本文主旨无关的提醒虽然是内部后台系统但管理员登录密码还是要做加盐Hash处理明文保存密码在任何地方都不可取设计前端服务有的读者可能更喜欢UI先行那么可以先看下面一节“设计登录页面”。有了跟后端一样的依赖注入我们可以将前端的认证也封装成服务。在项目中增加Services文件夹添加AuthService.cs文件using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization;internal class AuthService : IAuthService{private readonly HttpClient _httpClient;private readonly AuthenticationStateProvider _authenticationStateProvider;private readonly ILocalStorageService _localStorage;public AuthService(HttpClient httpClient,AuthenticationStateProvider authenticationStateProvider,ILocalStorageService localStorage){_httpClient httpClient;_authenticationStateProvider authenticationStateProvider;_localStorage localStorage;}public async Taskbool Login(LoginRqtDto rqtDto){var content new FormUrlEncodedContent(new KeyValuePairstring, string[]{new(nameof(LoginRqtDto.Account), rqtDto.Account),new(nameof(LoginRqtDto.Password), rqtDto.Password),});using var rsp await _httpClient.PostAsync(/account/login, content);if (!rsp.IsSuccessStatusCode){return false;}var authToken await rsp.Content.ReadAsStringAsync();await _localStorage.SetItemAsync(authToken, authToken);((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(rqtDto.Account);_httpClient.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, authToken);return true;}public async Task Logout(){await _localStorage.RemoveItemAsync(authToken);((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsLoggedOut();_httpClient.DefaultRequestHeaders.Authorization null;}} 首先要注意的是AuthenticationStateProvider这是一个抽象类由Microsoft.AspNetCore.Components.Authorization类库提供它用来提供当前用户的认证状态信息。既然是抽象类我们需要自定义一个它的子类基于JWT和LocalStorage实现它要求的规则即GetAuthenticationStateAsync方法using System.Security.Claims; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization;public class ApiAuthenticationStateProvider : AuthenticationStateProvider{private readonly HttpClient _httpClient;private readonly ILocalStorageService _localStorage;public ApiAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage){_httpClient httpClient;_localStorage localStorage;}public override async TaskAuthenticationState GetAuthenticationStateAsync(){var savedToken await _localStorage.GetItemAsyncstring(authToken);if (string.IsNullOrWhiteSpace(savedToken)){return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));}_httpClient.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, savedToken);return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), jwt)));}public void MarkUserAsAuthenticated(string account){var authenticatedUser new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, account) }, apiauth));var authState Task.FromResult(new AuthenticationState(authenticatedUser));NotifyAuthenticationStateChanged(authState);}public void MarkUserAsLoggedOut(){var anonymousUser new ClaimsPrincipal(new ClaimsIdentity());var authState Task.FromResult(new AuthenticationState(anonymousUser));NotifyAuthenticationStateChanged(authState);}private static IEnumerableClaim ParseClaimsFromJwt(string jwt){var claims new ListClaim();var payload jwt.Split(.)[1];var jsonBytes ParseBase64WithoutPadding(payload);var keyValuePairs JsonSerializer.DeserializeDictionarystring, object(jsonBytes);if (keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles) roles is string rolesText){if (rolesText.StartsWith([)){var parsedRoles JsonSerializer.Deserializestring[](rolesText);foreach (var parsedRole in parsedRoles){claims.Add(new Claim(ClaimTypes.Role, parsedRole));}}else{claims.Add(new Claim(ClaimTypes.Role, rolesText));}keyValuePairs.Remove(ClaimTypes.Role);}claims.AddRange(keyValuePairs.Select(kvp new Claim(kvp.Key, kvp.Value.ToString())));return claims;}private static byte[] ParseBase64WithoutPadding(string base64){switch (base64.Length % 4){case 2: base64 ; break;case 3: base64 ; break;}return Convert.FromBase64String(base64);}} 逻辑并不复杂。以上代码需要读者对JWT和System.Security.Claims类库比较熟悉建议初学者动手实践和调试。ILocalStorageService自然是由上文提到的Blazor.LocalStorage类库依赖注入。之前系列文章都提到了Blazor在.NET全栈开发下具有极大的开发效率优势。这里就有体现——既然后端已经提供了接口注意到LoginRqtDto类using System.ComponentModel.DataAnnotations;public class LoginRqtDto{[Display(Name 账号)][Required][StringLength(20, MinimumLength 3)]public string Account { get; set; }[Display(Name 密码)][Required][StringLength(20, MinimumLength 5]public string Password { get; set; }} 我们自然可以将该类放到Shared项目中使得前端Blazor项目在调用Login接口时可以不必再另写请求参数的Model。另外不单单是类本身的属性特性也可以被前后端共同利用这一点放到下文再讲。写完了该服务可别忘了依赖注入我的习惯是让Program.cs里的代码尽可能精简因此我会创建一个Extensions文件夹添加ServiceCollectionExtension.cs文件using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection;internal static class ServiceCollectionExtension{public static IServiceCollection AddAuth(this IServiceCollection services){services.AddAuthorizationCore().AddScopedAuthenticationStateProvider, ApiAuthenticationStateProvider().AddScopedIAuthService, AuthService();return services;}} 现在只需要在Program.cs里加一行代码builder.Services.AddAuth(); 设计登录页面登录页面的独特之处在于布局。例如内容页面是有侧边导航栏的但登录页面显然就没什么必要了。因此我建议单独写一个LoginLayout组件和默认布局MainLayout分开只用于Login页面inherits LayoutComponentBaseLayout Stylepadding:0;margin:0Header Styleheight:10%div stylemargin:10px;AntDesign.Row Justifyspace-around AlignmiddleAntDesign.Col Span8img src/imgs/logo.png stylealign-self:center //AntDesign.ColAntDesign.Col Span8 Offset8 Styletext-align:centerspan stylecolor:white; font-size:24px欢迎使用 ProductionName 后台管理系统/span/AntDesign.Col/AntDesign.Row/div/HeaderContent Stylebackground-color:white; min-height:500pxAntDesign.RowAntDesign.Col Span20 Offset2div stylemargin:100px 0Body/div/AntDesign.Col/AntDesign.Row/ContentMyFooter / /Layoutcode {private const string ProductionName Demo; }借助于Antd的Layout和Grid组件可以很轻松地搭建整个Login页面的布局这里我采用了最简单的上中下三层布局。注意到BodyBody是一种约定命名表示布局内的页面主体。对Login页面来说Body其实就是账户输入、密码输入和登录按钮。让我们在Pages文件夹里添加一个Login.razorpage /login layout LoginLayout inject NavigationManager NavigationManager inject MessageService MsgService inject IAuthService AuthServiceAntDesign.Form Model_loginData Styleheight:100%OnFinishOnFinishLabelColSpan4WrapperColSpan4FormItem WrapperColOffset10 WrapperColSpan4AntDesign.Input Placeholder请输入账号 AllowCleartrue bind-Valuecontext.AccountPrefixIcon Typeuser/Icon/Prefix/AntDesign.Input/FormItemFormItem WrapperColOffset10 WrapperColSpan4InputPassword Placeholder请输入密码 bind-Valuecontext.PasswordPrefixIcon Typelock/Icon/Prefix/InputPassword/FormItemFormItem WrapperColOffset11 WrapperColSpan2Button TypeButtonType.Primary HtmlTypesubmit Block登录/Button/FormItem /AntDesign.Formcode {private LoginRqtDto _loginData new();private async Task OnFinish(EditContext editContext){var result await AuthService.Login(_loginData);if (!result){await MsgService.Error(帐号或密码错误);return;}await MsgService.Success(登录成功);NavigationManager.NavigateTo(/home);} }我们使用layout指令来指定当前页面组件使用哪一种布局使用Antd提供的Form组件可以很方便地完成控件布局并添加提交功能再一次使用LoginRqtDto类将其属性与控件的值双向绑定实现最大化代码复用使用依赖注入在页面内方便地调用内置的NavigationManager和Antd提供的MessageService分别用于页面跳转和消息提示。页面效果如下登录页面依赖于Antd组件的出色实现诸如密码的开闭显示等细节都不必我们手动实现。还有一些细节并未在上面的代码里体现。例如后端使用System.ComponentModel.DataAnnotations类库可以很方便地对接口参数进行校验如上文提到的LoginRqtDto类。那么同样是使用C#Blazor是否也可以这样做呢当然可以Antd组件同样利用了接口参数的校验特性相较于一般前后端开发都需要通过API文档、团队纪律和组织沟通来保证前后端各种数据和逻辑的一致性。而使用Blazor开发在代码层面就可以天然地让前后端的行为一致只要让定义接口的人将自己的数据放到Shared项目里即可。前端校验提示关于上图有过Antd-Blazor开发经验的读者可能会好奇这里校验提示为什么是中文而不是默认的英文我将在下文“本地化校验提示”做简要说明。使用AuthorizeView组件动态显示内容登录页面及服务设计好之后还没有结束。对SPA应用来说每个页面有自己单独的路由用户可以手动输入路由绕过登录页面来访问其他页面。我们理所应当地希望如果用户未登录或认证失败那么其他页面对用户将不提供任何有价值的数据。对后端来说数据相关的接口都必须加上[Authorize]特性以校验访问者的身份。对前端来说应当以友好的方式提示用户登录而不是依旧发送页面请求依赖后端接口返回401或403再手动处理。MainLayout和AuthorizeView组件可以帮助我们统一处理这种情况。使用AuthorizeView组件之前我们需要在App.razor文件里使用CascadingAuthenticationState组件包裹Router组件using Microsoft.AspNetCore.Components.AuthorizationCascadingAuthenticationStateRouter AppAssemblytypeof(Program).Assembly PreferExactMatchestrueFound ContextrouteDataAuthorizeRouteView RouteDatarouteData DefaultLayouttypeof(MainLayout) //FoundNotFound MyNotFound //NotFound/Router /CascadingAuthenticationStateAntContainer /然后在MainLayout的Content部分使用AuthorizeView组件 Content Stylebackground-color:white; min-height:500pxAuthorizeViewAuthorizedBody/AuthorizedNotAuthorizeddiv stylemargin: 100px 0; width:100%; text-align: center; color: red;span stylefont-size:20px检测到登录超时请重新a href/login styletext-decoration:underline登录/a/span/div/NotAuthorized/AuthorizeViewBackTop/BackTop/Content单从标签命名上看就很容易理解认证通过则显示Body的内容否则显示一行字提示用户访问登录页。让我们看下不登录情况下直接访问Home首页的效果NotAuthorized时的Content这样对于默认使用MainLayout布局的其他所有页面若用户未认证则只会显示上图的效果。同理我们可以实现布局的Header部分动态显示未认证情况下不应显示上方“首页/关于”导航栏和右上方的账号信息这里本文不再赘述。本地化校验提示至此本文核心内容都已经结束了。但在编写登录页面的过程中有一个细节值得一提。在设计登录页面一节中我提到了前端校验提示。目前Antd组件在校验提示上还是使用System.ComponentModel.DataAnnotations类库的默认提示提示是全英文的。在上文提到的LoginRqtDto中我们可以使用Display特性来修改校验失败提示时属性的展示名称。但并不能修改整个提示的内容因此读者只会看到中英文混合的一段提示文本。注意到校验特性的父类ValidationAttribute有ErrorMessageResourceName和ErrorMessageResourceType两个属性。也就是说该父类在设计上是支持本地化的我们可以创建Resource资源来替换类库默认的错误提示。在XXX.Shared项目中创建Resources文件夹添加一个DA_zh_CN.resx文件命名随意中文提示资源IDE VS会自动生成一个的DA_zh_CN.designer.cs文件为你创建DA_zh_CN类。将上文提到的LoginRqtDto改为 public class LoginRqtDto{[Display(Name 账号)][Required(ErrorMessageResourceName RequiredError, ErrorMessageResourceType typeof(Resources.DA_zh_CN))][StringLength(20, MinimumLength 3, ErrorMessageResourceName StringLengthError_IncludingMin, ErrorMessageResourceType typeof(Resources.DA_zh_CN))]public string Account { get; set; }[Display(Name 密码)][Required(ErrorMessageResourceName RequiredError, ErrorMessageResourceType typeof(Resources.DA_zh_CN))][StringLength(20, MinimumLength 5, ErrorMessageResourceName StringLengthError_IncludingMin, ErrorMessageResourceType typeof(Resources.DA_zh_CN))]public string Password { get; set; }} 好了收工。这里resx文件里“名称”列我也不是随意取的而是照搬官方源码里的名称。有兴趣的读者可以参阅System.ComponentModel.DataAnnotations类库的相关源码。我也希望未来能有更简单的方式实现控件本地化校验提示。结束语下一篇文章会简单许多我将介绍如何使用Antd的Card组件和优雅的Razor语法做一个可灵活配置的、用于导航的首页。再会
http://wiki.neutronadmin.com/news/181227/

相关文章:

  • 怎么建设一个手机网站软件开发在大学属于什么专业
  • 北京p2p网站建设绿色资源网下载
  • 重庆响应式网站方案东莞知名企业
  • 论网站建设技术的作者是谁大型网站 中小型网站
  • 网站相对路径和绝对路径wordpress m3u8
  • 昆山网站制作哪家好游戏软件开发公司排名
  • 门户网站模板 免费vx小程序怎么开发
  • 网站空间的价格网页制作免费的素材网站
  • 福州公司做网站手机商务彩铃制作教程
  • 网站编程脚本语言建设银行流水账网站查询
  • 深圳龙华做网站公司网站建设的最新技术
  • 网站建设和使用情况揭阳市seo上词外包
  • 广东网站设计的公司福州本地推广
  • 网站建设与开发英文文献关键词挖掘查询工具爱站网
  • 公司网站开发实训报告网站的主流趋势
  • 小程序网站开发运行合同封面型网页网站有哪些
  • 用wordpress开发网站模板免费创建论坛网站
  • 上海网站建设工作室工程资料外包公司
  • 做网站名词网站建设费用模板
  • 网站订制公司自媒体wordpress
  • 网站建设云南找人做仿网站
  • 轮播网站响应式网站 谷歌 移动网站
  • 北京高端网站建设优势开源展示型网站
  • 代理会计公司网站模版长沙网站建站公司
  • 建一个网站大概需要多少钱百度公司可以做网站么
  • 护肤品网站建设方案设计素材网站
  • php手机网站如何制作教程wordpress文字编辑器
  • 外国网站签到做任务每月挣钱展示型手机网站模板下载
  • 一个网站怎么做软件好用吗规划建立一个网站
  • 需要推销自己做网站的公司公关到底做什么