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

ASP.NET Core 最小 API:极简开发,高效构建(下)

在上篇文章 ASP.NET Core 最小 API:极简开发,高效构建(上) 中我们添加了 API 代码并且测试,本篇继续补充相关内容。

一、使用 MapGroup API

示例应用代码每次设置终结点时都会重复 todoitems URL 前缀。 API 通常具有带常见 URL 前缀的终结点组,并且 MapGroup 方法可用于帮助组织此类组。 它减少了重复代码,并允许通过对 RequireAuthorizationWithMetadata 等方法的单一调用来自定义整个终结点组。

将 Program.cs 的内容替换为以下代码:

using Microsoft.EntityFrameworkCore;
using NSwag.AspNetCore;
using TodoApi;var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{config.DocumentName = "TodoAPI";config.Title = "TodoAPI v1";config.Version = "v1";
});var app = builder.Build();if (app.Environment.IsDevelopment())
{app.UseOpenApi();app.UseSwaggerUi(config =>{config.DocumentTitle = "TodoAPI";config.Path = "/swagger";config.DocumentPath = "/swagger/{documentName}/swagger.json";config.DocExpansion = "list";});
}var todoItems = app.MapGroup("/todoitems");todoItems.MapGet("/", async (TodoDb db) =>await db.Todos.ToListAsync());todoItems.MapGet("/complete", async (TodoDb db) =>await db.Todos.Where(t => t.IsComplete).ToListAsync());todoItems.MapGet("/{id}", async (int id, TodoDb db) =>await db.Todos.FindAsync(id)is Todo todo? Results.Ok(todo): Results.NotFound());todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{db.Todos.Add(todo);await db.SaveChangesAsync();return Results.Created($"/todoitems/{todo.Id}", todo);
});todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{var todo = await db.Todos.FindAsync(id);if (todo is null) return Results.NotFound();todo.Name = inputTodo.Name;todo.IsComplete = inputTodo.IsComplete;await db.SaveChangesAsync();return Results.NoContent();
});todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{if (await db.Todos.FindAsync(id) is Todo todo){db.Todos.Remove(todo);await db.SaveChangesAsync();return Results.NoContent();}return Results.NotFound();
});app.Run();

前面的代码执行以下更改:

  • 添加 var todoItems = app.MapGroup("/todoitems"); 以使用 URL 前缀 /todoitems 设置组。
  • 将所有 app.Map<HttpVerb> 方法更改为 todoItems.Map<HttpVerb>
  • /todoitems 方法调用中移除 URL 前缀 Map<HttpVerb>

二、使用 TypedResults API

返回 TypedResults(而不是 Results)有几个优点,包括可测试性和自动返回 OpenAPI 的响应类型元数据来描述终结点。有关详细信息,请参阅 TypedResults 与 Results。

使用以下代码更新 Program.cs:

using Microsoft.EntityFrameworkCore;
using NSwag.AspNetCore;
using TodoApi;var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{config.DocumentName = "TodoAPI";config.Title = "TodoAPI v1";config.Version = "v1";
});var app = builder.Build();if (app.Environment.IsDevelopment())
{app.UseOpenApi();app.UseSwaggerUi(config =>{config.DocumentTitle = "TodoAPI";config.Path = "/swagger";config.DocumentPath = "/swagger/{documentName}/swagger.json";config.DocExpansion = "list";});
}var todoItems = app.MapGroup("/todoitems");todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);app.Run();static async Task<IResult> GetAllTodos(TodoDb db)
{return TypedResults.Ok(await db.Todos.ToArrayAsync());
}static async Task<IResult> GetCompleteTodos(TodoDb db)
{return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}static async Task<IResult> GetTodo(int id, TodoDb db)
{return await db.Todos.FindAsync(id)is Todo todo? TypedResults.Ok(todo): TypedResults.NotFound();
}static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{db.Todos.Add(todo);await db.SaveChangesAsync();return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{var todo = await db.Todos.FindAsync(id);if (todo is null) return TypedResults.NotFound();todo.Name = inputTodo.Name;todo.IsComplete = inputTodo.IsComplete;await db.SaveChangesAsync();return TypedResults.NoContent();
}static async Task<IResult> DeleteTodo(int id, TodoDb db)
{if (await db.Todos.FindAsync(id) is Todo todo){db.Todos.Remove(todo);await db.SaveChangesAsync();return TypedResults.NoContent();}return TypedResults.NotFound();
}

Map<HttpVerb> 代码现在调用方法,而不是 lambda:

var todoItems = app.MapGroup("/todoitems");todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

这些方法返回实现 IResult 并由 TypedResults 定义的对象:

static async Task<IResult> GetAllTodos(TodoDb db)
{return TypedResults.Ok(await db.Todos.ToArrayAsync());
}static async Task<IResult> GetCompleteTodos(TodoDb db)
{return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}static async Task<IResult> GetTodo(int id, TodoDb db)
{return await db.Todos.FindAsync(id)is Todo todo? TypedResults.Ok(todo): TypedResults.NotFound();
}static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{db.Todos.Add(todo);await db.SaveChangesAsync();return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{var todo = await db.Todos.FindAsync(id);if (todo is null) return TypedResults.NotFound();todo.Name = inputTodo.Name;todo.IsComplete = inputTodo.IsComplete;await db.SaveChangesAsync();return TypedResults.NoContent();
}static async Task<IResult> DeleteTodo(int id, TodoDb db)
{if (await db.Todos.FindAsync(id) is Todo todo){db.Todos.Remove(todo);await db.SaveChangesAsync();return TypedResults.NoContent();}return TypedResults.NotFound();
}

三、防止过度发布

目前,示例应用公开了整个 Todo 对象。 在生产应用中,通常使用模型的一个子集来限制可以输入和返回的数据。 这背后有多种原因,但安全性是主要原因。 模型的子集通常称为数据传输对象 (DTO)、输入模型或视图模型。 本文使用的是 DTO。

DTO 可以用于:

  • 防止过度发布。
  • 隐藏客户端不应查看的属性。
  • 省略某些属性以减少有效负载大小。
  • 平展包含嵌套对象的对象图。 对客户端而言,平展的对象图可能更方便。

更新 Todo 类,使其包含机密字段:

public class Todo
{public int Id { get; set; }public string? Name { get; set; }public bool IsComplete { get; set; }public string? Secret { get; set; }
}

此应用需要隐藏机密字段,但管理应用可以选择公开它。使用以下代码创建名为 TodoItemDTO.cs 的文件:

public class TodoItemDTO
{public int Id { get; set; }public string? Name { get; set; }public bool IsComplete { get; set; }public TodoItemDTO() { }public TodoItemDTO(Todo todoItem) =>(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

Program.cs 文件的内容替换为以下代码以使用此 DTO 模型:

using Microsoft.EntityFrameworkCore;
using NSwag.AspNetCore;
using TodoApi;var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{config.DocumentName = "TodoAPI";config.Title = "TodoAPI v1";config.Version = "v1";
});var app = builder.Build();if (app.Environment.IsDevelopment())
{app.UseOpenApi();app.UseSwaggerUi(config =>{config.DocumentTitle = "TodoAPI";config.Path = "/swagger";config.DocumentPath = "/swagger/{documentName}/swagger.json";config.DocExpansion = "list";});
}app.MapGet("/todoitems", async (TodoDb db) =>await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>await db.Todos.FindAsync(id)is Todo todo? Results.Ok(new TodoItemDTO(todo)): Results.NotFound());app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{var todoItem = new Todo{IsComplete = todoItemDTO.IsComplete,Name = todoItemDTO.Name};db.Todos.Add(todoItem);await db.SaveChangesAsync();return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{var todo = await db.Todos.FindAsync(id);if (todo is null) return Results.NotFound();todo.Name = todoItemDTO.Name;todo.IsComplete = todoItemDTO.IsComplete;await db.SaveChangesAsync();return Results.NoContent();
});app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{if (await db.Todos.FindAsync(id) is Todo todo){db.Todos.Remove(todo);await db.SaveChangesAsync();return Results.NoContent();}return Results.NotFound();
});app.Run();

运行效果,

在这里插入图片描述

在这里插入图片描述

四、配置 JSON 序列化选项

1、全局配置 JSON 序列化选项

可通过调用 ConfigureHttpJsonOptions 来全局配置应用的选项。 以下示例包含公共字段,并设置 JSON 输出的格式。

var builder = WebApplication.CreateBuilder(args);builder.Services.ConfigureHttpJsonOptions(options => {options.SerializerOptions.WriteIndented = true;options.SerializerOptions.IncludeFields = true;
});var app = builder.Build();app.MapPost("/", (Todo todo) => {if (todo is not null) {todo.Name = todo.NameField;}return todo;
});app.Run();class Todo {public string? Name { get; set; }public string? NameField;public bool IsComplete { get; set; }
}

运行效果:

在这里插入图片描述

当请求报文为

{"name": "test","isComplete": true
}

则返回

{"name": null,"isComplete": true,"nameField": null
}

在这里插入图片描述

当请求报文为

{"nameField": "Walk dog","isComplete": false
}

则返回

{"name": "Walk dog","isComplete": false,"nameField": "Walk dog"
}

2、为终结点配置 JSON 序列化选项

若要为终结点配置序列化选项,请调用 Results.Json 并向其传递 JsonSerializerOptions 对象,如以下示例所示:

using System.Text.Json;var app = WebApplication.Create();var options = new JsonSerializerOptions(JsonSerializerDefaults.Web){ WriteIndented = true };app.MapGet("/", () => Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));app.Run();class Todo
{public string? Name { get; set; }public bool IsComplete { get; set; }
}

运行效果,

在这里插入图片描述

或者,使用接受 JsonSerializerOptions 对象的 WriteAsJsonAsync 的重载。 以下示例使用此重载设置输出 JSON 的格式:

using System.Text.Json;var app = WebApplication.Create();var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {WriteIndented = true };app.MapGet("/", (HttpContext context) =>context.Response.WriteAsJsonAsync<Todo>(new Todo { Name = "Walk dog", IsComplete = false }, options));app.Run();class Todo
{public string? Name { get; set; }public bool IsComplete { get; set; }
}

运行效果:

在这里插入图片描述

五、最小 API 中的身份验证和授权

1、有关身份验证和授权的关键概念

身份验证是确定用户标识的过程。 授权是确定用户是否有权访问资源的过程。 在 ASP.NET Core 中,身份验证和授权方案都有类似的实现语义。 身份验证由身份验证服务 IAuthenticationService 负责,而它供身份验证中间件使用。 授权由授权服务 IAuthorizationService 负责,而它供授权中间件使用。

身份验证服务会使用已注册的身份验证处理程序来完成与身份验证相关的操作。 例如,与身份验证相关的操作是对用户进行身份验证或注销用户。 身份验证方案是用于唯一地标识身份验证处理程序及其配置选项的名称。 身份验证处理程序负责实现身份验证策略,并在给定特定身份验证策略(如 OAuth 或 OIDC)的情况下生成用户的声明。 配置选项也是策略独有的,并为处理程序提供会影响身份验证行为的配置,例如重定向 URI。

在授权层中,有两种策略可用于确定用户对资源的访问权限:

  • 基于角色的策略根据所分配的角色(例如 Administrator 或 User)确定用户的访问权限。
  • 基于声明的策略根据中央颁发机构颁发的声明来确定用户的访问权限。

在 ASP.NET Core 中,这两种策略都被捕获到授权要求中。 授权服务利用授权处理程序来确定特定用户是否满足应用于资源的授权要求。

2、在最小应用中启用身份验证

若要启用身份验证,请调用 AddAuthentication 以对应用的服务提供商注册所需的身份验证服务。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication();
var app = builder.Build();app.MapGet("/", () => "Hello World!");
app.Run();

通常情况下,会使用一个特定的身份验证策略。 在以下示例中,应用被配置为支持基于 JWT 持有者的身份验证。 此示例使用 Microsoft.AspNetCore.Authentication.JwtBearer NuGet 包中提供的 API。

var builder = WebApplication.CreateBuilder(args);
// Requires Microsoft.AspNetCore.Authentication.JwtBearer
builder.Services.AddAuthentication().AddJwtBearer();
var app = builder.Build();app.MapGet("/", () => "Hello World!");
app.Run();

默认情况下,如果启用了某些身份验证和授权服务,WebApplication 会自动注册身份验证和授权中间件。 在以下示例中,无需调用 UseAuthenticationUseAuthorization 即可注册中间件,因为在调用 WebApplicationAddAuthentication 后,AddAuthorization 会自动执行此操作。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
var app = builder.Build();app.MapGet("/", () => "Hello World!");
app.Run();

具体原理可以查阅最小 API 应用中的中间件。在某些情况(例如控制中间件顺序)下,需要显式注册身份验证和授权。 在以下示例中,身份验证中间件是在 CORS 中间件运行后运行的。

var builder = WebApplication.CreateBuilder(args);builder.Services.AddCors();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();var app = builder.Build();app.UseCors();
app.UseAuthentication();
app.UseAuthorization();app.MapGet("/", () => "Hello World!");
app.Run();

3、配置身份验证策略

身份验证策略通常支持通过选项加载的各种配置。 对于以下身份验证策略,最小应用支持从配置加载选项:

  • 基于 JWT 持有者
  • 基于 OpenID 连接

ASP.NET Core 框架期望在Authentication:Schemes:{SchemeName}节的配置部分下找到这些选项。 在以下示例中,两个不同的方案 BearerLocalAuthIssuer 是使用各自的选项定义的。 Authentication:DefaultScheme 选项可用于配置所使用的默认身份验证策略。

{"Authentication": {"DefaultScheme":  "LocalAuthIssuer","Schemes": {"Bearer": {"ValidAudiences": ["https://localhost:7259","http://localhost:5259"],"ValidIssuer": "dotnet-user-jwts"},"LocalAuthIssuer": {"ValidAudiences": ["https://localhost:7259","http://localhost:5259"],"ValidIssuer": "local-auth"}}}
}

Program.cs 中,注册了两个基于 JWT 持有者的身份验证策略,其中:

  • “Bearer”方案名称。
  • “LocalAuthIssuer”方案名称。

“Bearer”是支持基于 JWT 持有者的应用中的一个典型默认方案,但可以通过设置 DefaultScheme 属性来替代默认方案,如前面的示例所示。

方案名称用于唯一地标识身份验证策略,并在从配置解析身份验证选项时用作查找键,如以下示例所示:

var builder = WebApplication.CreateBuilder(args);builder.Services.AddAuthentication().AddJwtBearer().AddJwtBearer("LocalAuthIssuer");var app = builder.Build();app.MapGet("/", () => "Hello World!");
app.Run();

4、在最小应用中配置授权策略

身份验证用于根据 API 识别和验证用户的标识。 授权用于验证和证实对 API 中资源的访问,并由通过 AddAuthorization 扩展方法注册的 IAuthorizationService 提供便利。 在以下方案中,添加了 /hello 资源,该资源要求用户提供一个 admin 角色声明以及 greetings_api 范围声明。

配置资源上的授权要求的过程分为两个步骤,需要:

  1. 全局配置策略中的授权要求。
  2. 将各个策略应用于资源。

在以下代码中,将调用 AddAuthorizationBuilder,其作用是:

  • 将与授权相关的服务添加到 DI 容器。
  • 返回一个 AuthorizationBuilder,它可用于直接注册身份验证策略。

该代码创建了一个名为 admin_greetings 的新授权策略,该策略封装了两个授权要求:

  • 一个通过 RequireRole 实现的基于角色的要求,面向具有 admin 角色的用户。
  • 一个通过 RequireClaim 实现的基于声明的要求,即用户必须提供 greetings_api 范围声明。

admin_greetings 策略作为 /hello 终结点所需的策略提供。

using Microsoft.Identity.Web;var builder = WebApplication.CreateBuilder(args);builder.Services.AddAuthorizationBuilder().AddPolicy("admin_greetings", policy =>policy.RequireRole("admin").RequireClaim("scope", "greetings_api"));var app = builder.Build();app.MapGet("/hello", () => "Hello world!").RequireAuthorization("admin_greetings");app.Run();

若无权限,则直接返回 401 Unauthorized 错误。

在这里插入图片描述

六、使用 dotnet user-jwts 进行开发测试

本文使用配置了基于 JWT 持有者的身份验证的应用。 基于 JWT 令牌持有者的身份验证要求客户端在请求头中呈现令牌,以验证其身份和声明。 通常,这些令牌由中央颁发机构颁发,例如标识服务器。

在本地计算机上进行开发时,dotnet user-jwts 工具可用于创建持有者令牌。

dotnet user-jwts create

注意

在项目上调用时,该工具会自动将与生成的令牌匹配的身份验证选项添加到 appsettings.json

可以通过多种自定义方式配置令牌。 例如,若要为上述代码中的授权策略所需的 admin 角色和 greetings_api 范围创建令牌,请执行以下操作:

dotnet user-jwts create --scope "greetings_api" --role "admin"

然后,可以在所选的测试工具中将生成的令牌作为标头的一部分发送。 举个使用 curl 的例子:

curl -i -H "Authorization: Bearer {token}" http://localhost:{port}/hello

在这里插入图片描述

(base) sam@sam-PC:/data/home/sam/MyWorkSpace/DotNetCoreWorkSpace/TodoApi$ dotnet user-jwts create --scope "greetings_api" --role "admin"
New JWT saved with ID 'b0fafeb4'.
Name: sam
Roles: [admin]
Scopes: greetings_apiToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6InNhbSIsInN1YiI6InNhbSIsImp0aSI6ImIwZmFmZWI0Iiwic2NvcGUiOiJncmVldGluZ3NfYXBpIiwicm9sZSI6ImFkbWluIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6MzQ0MiIsImh0dHBzOi8vbG9jYWxob3N0OjQ0MzgwIiwiaHR0cDovL2xvY2FsaG9zdDo1MDI2IiwiaHR0cHM6Ly9sb2NhbGhvc3Q6NzE1MyJdLCJuYmYiOjE3NDUwNTcwNDcsImV4cCI6MTc1MjkxOTQ0NywiaWF0IjoxNzQ1MDU3MDQ4LCJpc3MiOiJkb3RuZXQtdXNlci1qd3RzIn0.Rwgp9wO9PCLwJEiVgN-CGwlnLaAu0jhQXuzeo8Wh6Zg
curl -i -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6InNhbSIsInN1YiI6InNhbSIsImp0aSI6ImIwZmFmZWI0Iiwic2NvcGUiOiJncmVldGluZ3NfYXBpIiwicm9sZSI6ImFkbWluIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6MzQ0MiIsImh0dHBzOi8vbG9jYWxob3N0OjQ0MzgwIiwiaHR0cDovL2xvY2FsaG9zdDo1MDI2IiwiaHR0cHM6Ly9sb2NhbGhvc3Q6NzE1MyJdLCJuYmYiOjE3NDUwNTcwNDcsImV4cCI6MTc1MjkxOTQ0NywiaWF0IjoxNzQ1MDU3MDQ4LCJpc3MiOiJkb3RuZXQtdXNlci1qd3RzIn0.Rwgp9wO9PCLwJEiVgN-CGwlnLaAu0jhQXuzeo8Wh6Zg" http://localhost:5026/hello

在这里插入图片描述

可以看到,携带 token 请求可以正常访问 /hello 接口。完整代码如下:

using Microsoft.Identity.Web;var builder = WebApplication.CreateBuilder(args);builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();builder.Services.AddAuthorizationBuilder().AddPolicy("admin_greetings", policy =>policy.RequireRole("admin").RequireClaim("scope", "greetings_api"));var app = builder.Build();app.UseAuthorization();app.MapGet("/hello", () => "Hello world!").RequireAuthorization("admin_greetings");app.Run();

参考文档

  • 教程:使用 ASP.NET Core 创建最小 API

  • 最小 API 中的身份验证和授权

  • 使用 dotnet user-jwts 管理开发中的 JSON Web 令牌

相关文章:

  • Navicat、DataGrip、DBeaver在渲染 BOOLEAN 类型字段时的一种特殊“视觉风格”
  • XSS学习2
  • QT6 源(37):界面组件的总基类 QWidget 的源码阅读(下,c++ 代码部分)
  • 微服务与 SOA:架构异同全解析与应用指南
  • 【leetcode刷题日记】lc.300-最长递增子序列
  • 【WTYOLO】使用GPU训练YOLO模型教程记录
  • javaSE.队列
  • UE5的BumpOffset节点
  • 【英语语法】词法---形容词
  • 思维题专题
  • Agent安装-Beszel​​ 轻量级服务器监控平台
  • (4)Vue的生命周期详细过程
  • Python赋能去中心化电子商务平台:重构交易生态的新未来
  • 嵌入式人工智能应用-第三章 opencv操作 4 灰度处理
  • C++11特性补充
  • 图论基础:图存+记忆化搜索
  • 相得益彰 — 基于 GraphRAG 事理图谱驱动的实时金融行情新闻资讯洞察
  • Linux 常用指令用户手册
  • 字节跳动发布UI-TARS-1.5,入门AI就来近屿智能
  • 大数据学习栈记——MapReduce技术
  • 第1现场|俄乌互指对方违反复活节临时停火提议
  • 观察|中日航线加速扩容,航空公司如何抓住机会?
  • 《王牌对王牌》确认回归,“奔跑吧”将有主题乐园
  • 重庆警方通报“货车轮胎滚进服务区致人死亡”:正进一步调查
  • 中国足协、中足联:对浙江队外援阿隆·布彭扎不幸离世表示深切哀悼
  • 金科股份:控股股东被动减持收警告处罚与上市主体无关,对重整工作没有影响