IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> .Net Core 6 WebApi 项目搭建 -> 正文阅读

[游戏开发].Net Core 6 WebApi 项目搭建


前言

微软文档
刚开始上手Core 6,写博客记录一下。有不足之处请多指教。


一、创建第一个Core 6 项目

开发工具:Visual Studio 2022
Core版本:.Net Core 6

😁创建项目

Ⅰ.**创建新项目:**打开开发工具>创建新项目>搜索API>选择C#语言的ASP.NET Core Web API
在这里插入图片描述Ⅱ.**配置新项目:**自定义项目信息以及存储路径
在这里插入图片描述Ⅲ.其他信息:这里框架必须选择.NET 6.0,其他配置默认勾选即可,也可以根据自己的需求进行更改。(如果没有安装6.0,请看下一步)
在这里插入图片描述Ⅳ.更改开发工具安装
①打开开发工具,选择 继续但无需代码在这里插入图片描述

②导航栏>工具>获取工具和功能
在这里插入图片描述
③安装你需要的工具和功能
在这里插入图片描述

Ⅴ.创建完成之后就得到了一个最基础的Core6项目框架代码
在这里插入图片描述

😥Program.cs

Program.cs就是该项目程序的配置,在Core6中Program.cs 集成了 Startup.cs 的配置,详细的查看官网文档或者百度都行。因为我也是小白一枚请见谅🙏。
官网文档
迁移到 ASP.NET Core 6.0 中新的最小托管模型的代码示例

🤨创建Controller

可以先将系统默认创建的WeatherForecastController.cs和WeatherForecast.cs进行删除,然后在controller文件夹新建一个控制器,命名:UserController.cs

在这里插入图片描述在这里插入图片描述

在UserController中增加一个Hello的get方式的接口方法,返回"你好世界",通过请求api/user/hello来获取该接口的结果

点击运行后,开发工具会自动编译后启动。
会生成一个exe文件并自动运行,运行成功后会打开浏览器进行查看

在这里插入图片描述
在这里插入图片描述

由于在创建项目时我们已经勾选了Swagger服务,所以启动后的项目会使用Swagger UI 进行展示。

点击User接口>Try it out 就可以得到该接口的返回值及一些相关的属性
在这里插入图片描述

🤔总结

其实项目的创建以及控制器的创建和 core3是一致,只是.Net Core 6 给我的感觉就是比以往的版本更精简。可以将Swagger在创建项目时直接生成,而不需要手动的去配置。

二、读取appsettings.json

参考文章一
参考文章二

使用Nuget安装
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.Json
Microsoft.Extensions.Configuration.Binder
根据自己的开发环境选择合适的版本进行安装

Ⅰ、新建Helper文件夹用来存放帮助类,并添加一个AppSettings.cs
这里

	/// <summary>
    /// appsettings.json操作类
    /// </summary>
    public class Appsettings
    {
        static IConfiguration? Configuration { get; set; }
        static string? ContentPath { get; set; }

        public Appsettings(string contentPath)
        {
            string Path = "appsettings.json";

            //如果你把配置文件 是 根据环境变量来分开了,可以这样写
            //Path = $"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json";

            Configuration = new ConfigurationBuilder()
               .SetBasePath(contentPath)
               .Add(new JsonConfigurationSource { Path = Path, Optional = false, ReloadOnChange = true })//这样的话,可以直接读目录里的json文件,而不是 bin 文件夹下的,所以不用修改复制属性
               .Build();
        }

        public Appsettings(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        /// <summary>
        /// 封装要操作的字符
        /// </summary>
        /// <param name="sections">节点配置</param>
        /// <returns></returns>
        public static string App(params string[] sections)
        {
            try
            {

                if (sections.Any())
                {
#pragma warning disable CS8602 // 解引用可能出现空引用。
                    return Configuration[string.Join(":", sections)];
#pragma warning restore CS8602 // 解引用可能出现空引用。
                }
            }
            catch (Exception)
            {

            }

            return "";
        }

        /// <summary>
        /// 递归获取配置信息数组
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sections"></param>
        /// <returns></returns>
        public static List<T> App<T>(params string[] sections)
        {
            List<T> list = new();
            Configuration.Bind(string.Join(":", sections), list);
            return list;
        }
    }

Ⅱ、修改Program.cs
①在 var builder = WebApplication.CreateBuilder(args); 前增加

IConfiguration configuration = new ConfigurationBuilder()
                            .AddJsonFile("appsettings.json")
                            .Build();

②在 var builder = WebApplication.CreateBuilder(args); 后增加

builder.Services.AddSingleton(new Appsettings(configuration));

③读取
appsettings.json 代码

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AppSettings": {
    //数据库连接字符串
    "ConnectionString": "Server=127.0.0.1;User Id=用户id;Password=密码;Database=数据库名称;"
  }
}

读取

var text = Appsettings.App(new string[] { "AppSettings", "ConnectionString" });

在这里插入图片描述


三、修改Program.cs

😀跨域

官方文档
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
在 ASP.NET Core 中启用跨源请求 (CORS)
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
官网文档给出了不同的跨域请求解决方案,可以根据实际需求进行选择自己喜欢的方案。
我这里就配置最简单的 具有默认策略和中间件的 CORS

var builder = WebApplication.CreateBuilder(args); 后面添加如下代码;在这里我并没有针对到具体的地址,而是使用 AllowAnyOrigin() 确保策略允许任何源
这里 policy后面的方法可以查看👉CorsPolicyBuilder 类

方法属性
AllowAnyHeader()确保策略允许任何标头
AllowAnyMethod()确保策略允许任何方法
AllowAnyOrigin()确保策略允许任何源
AllowCredentials()设置策略以允许凭据
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        policy =>
        {
            policy.AllowAnyOrigin().AllowAnyHeader();
        });
});

注册中间件
?中间件顺序 ASP.NET Core 中间件
注册中间件时,顺序最好按照官网给出的依次进行注册,不然会无法生效

UseCors 添加 CORS 中间件。 对 UseCors 的调用必须放在 UseRouting 之后,但在 UseAuthorization 之前。

app.UseRouting();
//注册中间件
app.UseCors();

app.UseAuthorization();

完整代码

IConfiguration configuration = new ConfigurationBuilder()
                            .AddJsonFile("appsettings.json")
                            .Build();
var builder = WebApplication.CreateBuilder(args);
//读取配置文件
builder.Services.AddSingleton(new Appsettings(configuration));
var text = Appsettings.App(new string[] { "AppSettings", "ConnectionString" });
Console.WriteLine($"ConnectionString:{text}");
// Add services to the container.
//跨域
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
    });
});

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
//app.UseHttpsRedirection();
app.UseCors();
app.UseAuthorization();
app.MapControllers();
app.Run();

😁修改启动的Url

虽然修改启动的Url的并不需要动Program.cs文件中的配置,为了方便查看我还是放到这里来进行叙述。

其实这个也没啥好说的,只是Core6版的配置和之前的有那么一丢丢不一样,俗话说好记性不如烂笔头。所以我还是在这记录下来啦。
其实在查看官网文档👉Kestrel 终结点配置 之后你就会觉得So easy

Kestrel 特定的终结点配置将覆盖所有跨服务器终结点配置
找到项目中的 appsettings.json 文件,在该Josn文件中加入以下代码

"Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:7521"
      },
      "Https": {
        "Url": "https://localhost:7526"
      }
    }
  },
这里的IP地址根据core3的配置方法,我猜测可以将IP地址换为*号,只是开发工具会发出警告,因为部署到服务器上我想让他自己匹配服务器的IP,而不是我在这进行编辑。因为我也还没部署到服务器上,先大胆假设一下吧,等到后面我部署到服务器上在回来完善这一部分。

然后在启动项目后,项目的请求地址就会发生改变
在这里插入图片描述
在这里还有一个问题哈,就是在创建的时候如果默认勾选了HTTS服务,那么在项目创建完成之后会在Program.cs 的中间件中默认注册
app.UseHttpsRedirection();
在执行了上面修改启动URL的时候,如果此时加入了一个HTTP的URL,你会发现去请求这个HTTP下的接口服务时,它会告诉你请求不了,然而HTTS下的接口服务却正常。

原因:HTTPS 重定向中间件 (UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。

这时候只需要将app.UseHttpsRedirection();注释或者删除即可
在这里插入图片描述


四、启用JWT鉴权

什么是 JSON Web 令牌?
JSON Web Token (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,用于将信息作为 JSON 对象在各方之间安全地传输。此信息可以进行验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
JWT官网
JWT详解

网上对JWT的讲解有很多,我就不在这过多叙述啦。

😺生成Token令牌

在项目中找到appsettings.json,在appsettings.json中配置jwt参数的值 【注意】 SecretKey必须大于16个,是大于,不是大于等于。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "AppSettings": {
    //数据库连接字符串
    "ConnectionString": "Server=127.0.0.1;User Id=用户id;Password=密码;Database=数据库名称;",
    //JWT参数
    "JwtSetting": {
      "Issuer": "jwtIssuer", //颁发者
      "Audience": "jwtAudience", //可以给哪些客户端使用
      "SecretKey": "chuangqianmingyueguang" //加密的Key
    }
  }
}

😸安装需要的包

  1. IdentityModel
  2. Microsoft.AspNetCore.Authentication.JwtBearer
  3. Microsoft.AspNetCore.Authorization

在这里插入图片描述

😹新建TokenModel类

该类是JWT的返回实体类,创建位置根据你的项目情况而定,注意刚刚安装的包的位置!!!

/// <summary>
    /// 令牌
    /// </summary>
    public class TokenModel
    {
        /// <summary>
        /// Id
        /// </summary>
        public string? Uid { get; set; }
        /// <summary>
        /// 角色
        /// </summary>
        public string? Role { get; set; }

    }

😻新建JwtHelper.cs

注意建立的位置与刚刚安装的包的位置!!!
以下代码来自博客园

 public class JwtHelper
    {
        /// <summary>
        /// 颁发JWT字符串
        /// </summary>
        /// <param name="tokenModel"></param>
        /// <returns></returns>
        public static string IssueJwt(TokenModel tokenModel)
        {
            //获取Appsetting配置
            string iss = Appsettings.App(new string[] { "AppSettings", "JwtSetting", "Issuer" });
            string aud = Appsettings.App(new string[] { "AppSettings", "JwtSetting", "Audience" });
            string secret = Appsettings.App(new string[] { "AppSettings", "JwtSetting", "SecretKey" });

            //var claims = new Claim[] //old
            var claims = new List<Claim>
                {
                 /*
                 * 特别重要:
                   1、这里将用户的部分信息,比如 uid 存到了Claim 中,如果你想知道如何在其他地方将这个 uid从 Token 中取出来,请看下边的SerializeJwt() 方法,或者在整个解决方案,搜索这个方法,看哪里使用了!
                   2、你也可以研究下 HttpContext.User.Claims ,具体的你可以看看 Policys/PermissionHandler.cs 类中是如何使用的。
                 */



                new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ToString()),
                new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"),
                new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                //这个就是过期时间,目前是过期1000秒,可自定义,注意JWT有自己的缓冲过期时间
                new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds()}"),
                new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(1000).ToString()),
                new Claim(JwtRegisteredClaimNames.Iss,iss),
                new Claim(JwtRegisteredClaimNames.Aud,aud),


               };

            // 可以将一个用户的多个角色全部赋予;
            claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s)));



            //秘钥 (SymmetricSecurityKey 对安全性的要求,密钥的长度太短会报出异常)
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            var jwt = new JwtSecurityToken(
                issuer: iss,
                claims: claims,
                signingCredentials: creds);

            var jwtHandler = new JwtSecurityTokenHandler();
            var encodedJwt = jwtHandler.WriteToken(jwt);

            return encodedJwt;
        }

        /// <summary>
        /// 解析
        /// </summary>
        /// <param name="jwtStr"></param>
        /// <returns></returns>
        public static TokenModel SerializeJwt(string jwtStr)
        {
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
            object role;
            try
            {
                jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
            var tm = new TokenModel
            {
                Uid = jwtToken.Id.ToString(),
                Role = role != null ? role.ToString() : "",
            };
            return tm;
        }
    }

😼获取Token

UserController新建Login接口,用来获取token
注意一个控制器多个接口时[HttpGet(“Login”)]需要在方法后面加入接口名称

[HttpGet("Login")]
public IActionResult Login(string role)
 {
   string jwtStr = string.Empty;
    bool suc = false;
	if (role != null)
		{
                // 将用户id和角色名,作为单独的自定义变量封装进 token 字符串中。
                TokenModel tokenModel = new TokenModel { Uid = "abcde", Role = role };
                jwtStr = JwtHelper.IssueJwt(tokenModel);//登录,获取到一定规则的 Token 令牌
                suc = true;
            }
            else
            {
                jwtStr = "login fail!!!";
            }

            return Ok(new
            {
                success = suc,
                token = jwtStr
            });
        }

运行项目,请求Login接口输入参数即可获得Token值
在这里插入图片描述

😽Swagger中开启JWT服务

要测试 JWT 授权认证,就必定要输入 Token令牌,那怎么输入呢,平时的话,我们可以使用 Postman 来控制输入,就是在请求的时候,在 Header 中,添加Authorization属性。但是现在使用了 Swagger 作为接口文档,那怎么输入呢,其实 Swagger 已经帮我们实现了这个录入 Token令牌的功能。

在Program.cs文件中找到👉builder.Services.AddSwaggerGen();方法,将该方法改为以下所示
注意需要安装包 Swashbuckle.AspNetCore.Filters

//为swagger配置JWT
builder.Services.AddSwaggerGen(options =>
{
    // 在header中添加token,传递到后台
    //安装包 Swashbuckle.AspNetCore.Filters
    options.OperationFilter<SecurityRequirementsOperationFilter>();
    #region Token绑定到ConfigureServices
    options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
    {
        Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)\"",
        Name = "Authorization",//jwt默认的参数名称
        In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
        Type = SecuritySchemeType.ApiKey
    });
    #endregion
});

运行项目后在接口主页面右上方就能看到Token的入口。
在这里插入图片描述在这里插入图片描述

🙀JWT授权认证

新建AuthorizationSetup.cs
代码来自博客园

/// <summary>
    /// 身份验证服务
    /// </summary>
    public static class AuthorizationSetup
    {
        /// <summary>
        /// 注册身份验证服务
        /// </summary>
        /// <param name="services"></param>
        /// <exception cref="ArgumentNullException"></exception>
        public static void AddAuthorizationSetup(this IServiceCollection services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
            //读取配置文件
            var symmetricKeyAsBase64 = Appsettings.App(new string[] { "AppSettings", "JwtSetting", "SecretKey" });
            var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
            var signingKey = new SymmetricSecurityKey(keyByteArray);
            var Issuer = Appsettings.App(new string[] { "AppSettings", "JwtSetting", "Issuer" });
            var Audience = Appsettings.App(new string[] { "AppSettings", "JwtSetting", "Audience" });
            // 令牌验证参数
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,
                ValidateIssuer = true,
                ValidIssuer = Issuer,//发行人
                ValidateAudience = true,
                ValidAudience = Audience,//订阅人
                ValidateLifetime = true,
                ClockSkew = TimeSpan.FromSeconds(30),
                RequireExpirationTime = true,
            };

            //2.1【认证】、core自带官方JWT认证
            // 开启Bearer认证
            services.AddAuthentication("Bearer")
             // 添加JwtBearer服务
             .AddJwtBearer(o =>
             {
                 o.TokenValidationParameters = tokenValidationParameters;
                 o.Events = new JwtBearerEvents
                 {
                     OnAuthenticationFailed = context =>
                     {
                         // 如果过期,则把<是否过期>添加到,返回头信息中
                         if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                         {
                             context.Response.Headers.Add("Token-Expired", "true");
                         }
                         return Task.CompletedTask;
                     }
                 };
             });
        }
    }

在Program.cs中注册验证服务
官网文档👉ASP.NET Core 身份验证概述

//注册身份验证服务
builder.Services.AddAuthorizationSetup();

在这里插入图片描述注册中间件
ASP.NET Core 中间件顺序

app.UseRouting();
//身份验证
app.UseAuthentication();

app.UseAuthorization();

在这里插入图片描述

😿API接口授权策略

对接口进行授权的方式有两种:

直接对需要进行授权验证的接口 [Authorize] 加上这个配置信息
注意需要引入这个包Microsoft.AspNetCore.Authorization

[HttpGet("Hello")]
[Authorize]
public string Hello()
{
   return "你好世界";
}

第二种则是对整个控制器加上 [Authorize] 标识那么该控制器对于的所有接口都需要进行授权才能进行访问。
使用 **[AllowAnonymous]**可以取消授权验证

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using webapi.core.model;
using Webapi.Core.Common.Helper;

namespace WebApplication1.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [Authorize]
    public class UserController : ControllerBase
    {
        
        [HttpGet("Hello")]
        public string Hello()
        {
            return "你好世界";
        }
        
        [HttpGet("Login")]
        [AllowAnonymous]
        public IActionResult Login(string role)
        {
            string jwtStr = string.Empty;
            bool suc = false;

            if (role != null)
            {
                // 将用户id和角色名,作为单独的自定义变量封装进 token 字符串中。
                TokenModel tokenModel = new TokenModel { Uid = "abcde", Role = role };
                jwtStr = JwtHelper.IssueJwt(tokenModel);//登录,获取到一定规则的 Token 令牌
                suc = true;
            }
            else
            {
                jwtStr = "login fail!!!";
            }

            return Ok(new
            {
                success = suc,
                token = jwtStr
            });
        }
    }
}

😾 解析Token

主要是调用JwtHelper下的SerializeJwt方法

/// <summary>
/// 解析Token
/// </summary>
/// <returns></returns>
[HttpGet]
[Authorize]
public IActionResult ParseToken()
{
    //需要截取Bearer 
    var tokenHeader = HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
    var user = JwtHelper.SerializeJwt(tokenHeader);
    return Ok(user);
}

在这里我在记录一下如何获取payload的信息,因为有时候需要获取里面的信息进行返回

#region 获取payload信息
var handler = new JwtSecurityTokenHandler();
var payload = handler.ReadJwtToken(jwtStr).Payload;
var claims = payload.Claims;
var expireTime = claims.First(claim => claim.Type == "exp").Value;
#endregion

基于角色进行接口的授权,请查看文章👉Net core 3 JWT授权方法都是一样的所以我就不过多阐述,有问题可以直接私我

参考链接

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-04-15 00:35:02  更:2022-04-15 00:36:26 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 17:14:51-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码