為了在 Node.js 中 構(gòu)建 API,我們將使用 Nest.js。它是一個(gè)相當(dāng)靈活的框架,建立在 Express.js 的基礎(chǔ)上,可以讓你在短時(shí)間內(nèi)制作出 Node.js 服務(wù),因?yàn)樗闪撕芏嗪霉δ埽ㄈ缤耆念愋突С帧⒁蕾囎⑷搿⒛K管理和更多)。

項(xiàng)目和工具

為了更快地開始工作,Nest.js 附帶了一個(gè)很好的 CLI 工具,可以為我們創(chuàng)建項(xiàng)目模板。我們開始用以下幾行代碼生成我們的項(xiàng)目:

npm i -g @nestjs/cli 
nest new project-name

更多的 Nest.js 和它的 CLI

讓我們測(cè)試一下,看看到目前為止是否一切正常:

npm run start:dev

添加數(shù)據(jù)持久層

我們將使用 TypeORM 來管理我們的數(shù)據(jù)庫(kù)架構(gòu)。TypeORM 的優(yōu)點(diǎn)是:它可以讓你通過代碼來描述數(shù)據(jù)實(shí)體模型,然后能夠應(yīng)用和同步這些模型到表結(jié)構(gòu)的數(shù)據(jù)庫(kù)。(這不僅適用于 PostgreSQL 數(shù)據(jù)庫(kù),還適用于其他數(shù)據(jù)庫(kù),可以在 TypeORM 文檔中找到支持哪些數(shù)據(jù)庫(kù))

使用 docker 自動(dòng)化設(shè)置本地 PostgreSQL 數(shù)據(jù)庫(kù)實(shí)例。

要在本地實(shí)現(xiàn)數(shù)據(jù)持久性,我們現(xiàn)在需要一個(gè)數(shù)據(jù)庫(kù)服務(wù)器和一個(gè)要連接的數(shù)據(jù)庫(kù)。一種方法是在本地機(jī)器上設(shè)置一個(gè) PostgreSQL 數(shù)據(jù)庫(kù)服務(wù)器,但這樣做不是很好。因?yàn)檫@樣項(xiàng)目與我們的本地?cái)?shù)據(jù)庫(kù)服務(wù)器會(huì)過于耦合。這意味著如果你和一個(gè)團(tuán)隊(duì)一起做一個(gè)項(xiàng)目,只要切換機(jī)器就要在每臺(tái)機(jī)器上設(shè)置數(shù)據(jù)庫(kù)服務(wù)器,或者以某種方式編寫安裝指南等(當(dāng)你團(tuán)隊(duì)的開發(fā)同學(xué)有不同的操作系統(tǒng)時(shí),事情變得更加棘手)。
那么我們?nèi)绾慰朔@一點(diǎn)呢?讓這個(gè)步驟自動(dòng)化!
我們使用預(yù)構(gòu)建的 PostgreSQL docker 鏡像并將數(shù)據(jù)庫(kù)服務(wù)器作為 docker 進(jìn)程運(yùn)行。我們可以用幾行 shell 代碼編寫一個(gè)完整的設(shè)置來讓我們的服務(wù)器實(shí)例運(yùn)行并準(zhǔn)備一個(gè)空的數(shù)據(jù)庫(kù)準(zhǔn)備連接。因?yàn)樗强蓮?fù)用的,并且設(shè)置代碼可以與項(xiàng)目代碼的其余部分一起在源代碼管理中進(jìn)行管理,這使得團(tuán)隊(duì)中其他開發(fā)人員的 “入門” 變得非常簡(jiǎn)單。
下面是這個(gè)腳本的樣子:

#!/bin/bash
set -e

SERVER="my_database_server";
PW="mysecretpassword";
DB="my_database";

echo "echo stop & remove old docker [$SERVER] and starting new fresh instance of [$SERVER]"
(docker kill $SERVER || :) && \
(docker rm $SERVER || :) && \
docker run --name $SERVER -e POSTGRES_PASSWORD=$PW \
-e PGPASSWORD=$PW \
-p 5432:5432 \
-d postgres

# wait for pg to start
echo "sleep wait for pg-server [$SERVER] to start";
SLEEP 3;

# create the db
echo "CREATE DATABASE $DB ENCODING 'UTF-8';" | docker exec -i $SERVER psql -U postgres
echo "\l" | docker exec -i $SERVER psql -U postgres

讓我們將該命令添加到我們的 package.json 運(yùn)行腳本中,以便我們可以輕松執(zhí)行它。

"start:dev:db": "./src/scripts/start-db.sh"

現(xiàn)在我們有了一個(gè)可以運(yùn)行的命令,它會(huì)設(shè)置數(shù)據(jù)庫(kù)服務(wù)器和一個(gè)普通的數(shù)據(jù)庫(kù)。
為了使過程更健壯,我們將為 docker 容器使用相同的名稱(腳本中的 $SERVER var),并添加一個(gè)額外的檢查:如果有同名的容器正在運(yùn)行,那么將結(jié)束并刪除它以確保干凈狀態(tài)。

Nest.js 連接數(shù)據(jù)庫(kù)

就像所有事情一樣,已經(jīng)有一個(gè) NPM 模塊可以幫助您將 Nest.js 項(xiàng)目掛鉤到您的數(shù)據(jù)庫(kù)。讓我們使用預(yù)構(gòu)建的 NestJS-to-TypeORM 模塊為我們的項(xiàng)目添加 TypeORM 支持。
您可以像這樣添加所需的模塊:

npm install --save @nestjs/typeorm typeorm pg

配置管理

我們可以在 Nest.js 中配置 TypeORM 連接到哪個(gè)數(shù)據(jù)庫(kù)服務(wù)器,方法是使用 TypeOrmModule。它有一個(gè) forRoot 方法,我們可以傳入配置。我們知道配置在本地開發(fā)和生產(chǎn)環(huán)境中會(huì)有所不同。所以,這個(gè)過程在某種程度上必須是通用的,以便它可以在不同運(yùn)行環(huán)境提供不同的配置。我們可以編寫以下配置服務(wù)。這個(gè)配置類的功能是在我們的 API Server main.ts 啟動(dòng)之前運(yùn)行。它可以從環(huán)境變量中讀取配置,然后在運(yùn)行時(shí)以只讀方式提供值。為了使 dev 和 prod 靈活,我們將使用 dotenv 模塊。

npm install --save dotenv

有了這個(gè)模塊,我們可以在本地開發(fā)的項(xiàng)目根目錄中有一個(gè) “.env” 文件來準(zhǔn)備配置值,而在生產(chǎn)中,我們可以從生產(chǎn)服務(wù)器上的環(huán)境變量中讀取值。這是一種非常靈活的方法,還允許您使用一個(gè)文件輕松地與團(tuán)隊(duì)中的其他開發(fā)人員共享配置。注意:我強(qiáng)烈建議 git 忽略此文件,因?yàn)槟阌锌赡軙?huì)將生產(chǎn)環(huán)境的賬號(hào)密碼放入此文件中,所以你不應(yīng)把配置文件提交到項(xiàng)目中而造成意外泄露。
這是您的 .env 文件的樣子:

POSTGRES_HOST=127.0.0.1 
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=mysecretpassword
POSTGRES_DATABASE=my_database
PORT=3000
MODE=DEV
RUN_MIGRATIONS=true

因此,我們的 ConfigService 將作為單例服務(wù)運(yùn)行,在啟動(dòng)時(shí)加載配置值并將它們提供給其他模塊。我們將在服務(wù)中包含一個(gè)容錯(cuò)模式。這意味著如果獲取一個(gè)不存在的值,它將拋出含義完整的錯(cuò)誤。這使您的設(shè)置更加健壯,因?yàn)槟鷮⒃跇?gòu)建 / 啟動(dòng)時(shí)檢測(cè)配置錯(cuò)誤,而不是在運(yùn)行時(shí)生命周期。這樣您將能夠在部署 / 啟動(dòng)服務(wù)器時(shí)盡早地檢測(cè)到這一點(diǎn),而不是在消費(fèi)者使用您的 api 時(shí)才發(fā)現(xiàn)問題。
這是您的 ConfigService 的外觀以及我們將其添加到 Nest.js 應(yīng)用程序模塊的方式:

// app.module.ts

import { Module } from'@nestjs/common';
import { TypeOrmModule } from'@nestjs/typeorm';
import { AppController } from'./app.controller';
import { AppService } from'./app.service';
import { configService } from'./config/config.service';

@Module({
imports: [
TypeOrmModule.forRoot(configService.getTypeOrmConfig())
],
controllers: [AppController],
providers: [AppService],
})
exportclass AppModule { }
// src/config/config.service.ts

import { TypeOrmModuleOptions } from'@nestjs/typeorm';

require('dotenv').config();

class ConfigService {

constructor(private env: { [k: string]: string | undefined }) { }

private getValue(key: string, throwOnMissing = true): string {
const value = this.env[key];
if (!value && throwOnMissing) {
thrownewError(config error - missing env.${key}); } return value; } publicensureValues(keys: string[]) { keys.forEach(k =>this.getValue(k, true)); returnthis; } publicgetPort() { returnthis.getValue('PORT', true); } publicisProduction() { const mode = this.getValue('MODE', false); return mode != 'DEV'; } public getTypeOrmConfig(): TypeOrmModuleOptions { return { type: 'postgres', host: this.getValue('POSTGRES_HOST'), port: parseInt(this.getValue('POSTGRES_PORT')), username: this.getValue('POSTGRES_USER'), password: this.getValue('POSTGRES_PASSWORD'), database: this.getValue('POSTGRES_DATABASE'), entities: ['**/*.entity{.ts,.js}'], migrationsTableName: 'migration', migrations: ['src/migration/*.ts'], cli: { migrationsDir: 'src/migration', }, ssl: this.isProduction(), }; } } const configService = new ConfigService(process.env) .ensureValues([ 'POSTGRES_HOST', 'POSTGRES_PORT', 'POSTGRES_USER', 'POSTGRES_PASSWORD', 'POSTGRES_DATABASE' ]); export { configService };

開發(fā)重啟

npm i --save-dev nodemon ts-node

然后在 root 中添加一個(gè)帶有調(diào)試和 ts-node 支持的 nodemon.json 文件

{ 
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --inspect=127.0\. 0.1:9223 -r ts-node/register -- src/main.ts",
"env": {}
}

最后我們將 package.json 中的 start:dev 腳本更改為:

"start:dev": "nodemon --config nodemon.json",

這樣可以通過 npm run start:dev 來啟動(dòng)我們的 API-server,在啟動(dòng)時(shí)它應(yīng)該從 ConfigService 中獲取 .env 對(duì)應(yīng)環(huán)境的 values,然后將 typeORM 連接到我們的數(shù)據(jù)庫(kù),而且它不綁定在我的機(jī)器上。

定義和加載數(shù)據(jù)模型實(shí)體

TypeORM 支持自動(dòng)加載數(shù)據(jù)模型實(shí)體。您可以簡(jiǎn)單地將它們?nèi)糠旁谝粋€(gè)文件夾中,并在您的配置中使用一種模式加載它們 —— 我們將我們的放在 model/.entity.ts 中。(見實(shí)體的 TypeOrmModuleOptions 中的 ConfigService)

TypeORM 的另一個(gè)特性是這些實(shí)體模型支持繼承。
例如,如果您希望每個(gè)實(shí)體都擁有某些數(shù)據(jù)字段。
例如:自動(dòng)生成的 uuid id 字段 和 createDateTime 字段,lastChangedDateTime 字段。
注意:這些基類應(yīng)該是 abstract。
因此,在 TypeORM 中定義數(shù)據(jù)模型實(shí)體將如下所示:

// base.entity.ts

import { PrimaryGeneratedColumn, Column, UpdateDateColumn, CreateDateColumn } from'typeorm';

exportabstractclass BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'boolean', default: true })
isActive: boolean;

@Column({ type: 'boolean', default: false })
isArchived: boolean;

@CreateDateColumn({ type: 'timestamptz', default: () =>'CURRENT_TIMESTAMP' })
createDateTime: Date;

@Column({ type: 'varchar', length: 300 })
createdBy: string;

@UpdateDateColumn({ type: 'timestamptz', default: () =>'CURRENT_TIMESTAMP' })
lastChangedDateTime: Date;

@Column({ type: 'varchar', length: 300 })
lastChangedBy: string;

@Column({ type: 'varchar', length: 300, nullable: true })
internalComment: string | null;
}
// item.entity.ts

import { Entity, Column } from'typeorm';
import { BaseEntity } from'./base.entity';

@Entity({ name: 'item' })
exportclass Item extends BaseEntity {

@Column({ type: 'varchar', length: 300 })
name: string;

@Column({ type: 'varchar', length: 300 })
description: string;
}

在 typeORM 文檔中查找更多支持的數(shù)據(jù)注釋。
讓我們啟動(dòng)我們的 API,看看它是否有效。

npm run start:dev:db 
npm run start:dev

實(shí)際上我們的數(shù)據(jù)庫(kù)并沒有立即反映我們的數(shù)據(jù)模型,TypeORM 能夠?qū)⒛臄?shù)據(jù)模型同步到數(shù)據(jù)庫(kù)中的表中。數(shù)據(jù)模型自動(dòng)同步很好,但也很危險(xiǎn)。為什么?在前期開發(fā)中,您可能沒有把所有數(shù)據(jù)實(shí)體都整理清楚。因此,您在代碼中更改了實(shí)體類, typeORM 會(huì)為你自動(dòng)同步字段, 但是,一旦您的數(shù)據(jù)庫(kù)中有實(shí)際數(shù)據(jù),后期打算修改字段類型或其他操作時(shí),TypeORM 將通過刪除并重新創(chuàng)建數(shù)據(jù)庫(kù)表來更改數(shù)據(jù)庫(kù),這意味著你極有可能丟失了表內(nèi)的數(shù)據(jù)。當(dāng)然在生產(chǎn)環(huán)境中你應(yīng)該避免這種意想不到情況發(fā)生。
這就是為什么我更喜歡從一開始就直接在代碼中處理數(shù)據(jù)庫(kù)遷移。
這也將幫助您和您的團(tuán)隊(duì)更好地跟蹤和理解數(shù)據(jù)結(jié)構(gòu)的變化,并迫使您更積極地思考這一點(diǎn):怎樣做可以幫助您避免生產(chǎn)環(huán)境中的破壞性更改和數(shù)據(jù)丟失。
幸運(yùn)的是 TypeORM 提供了一個(gè)解決方案和 CLI 命令,它為你處理生成 SQL 命令的任務(wù)。然后,您可以輕松驗(yàn)證和測(cè)試這些,而無需在后臺(tái)使用任何黑魔法。
以下是如何設(shè)置 typeORM CLI 的最佳實(shí)踐。

1.typeORM CLI 的設(shè)置

我們已經(jīng)在 ConfigService 中添加了所有必要的配置,但是 typeORM CLI 與 ormconfig.json 是同時(shí)生效的,所以我們希望與正式環(huán)境的 CLI 區(qū)分開來。添加一個(gè)腳本來編寫配置 json 文件并將其添加到我們的.gitignore -list:

import fs = require('fs');
fs.writeFileSync('ormconfig.json', JSON.stringify(configService.getTypeOrmConfig(), null, 2)
);

添加一個(gè) npm 腳本任務(wù)來運(yùn)行它以及 typeorm:migration:generate 和 typeorm:migration:run 的命令。
像這樣 ormconfig 將在運(yùn)行 typeORM CLI 命令之前生成。

"pretypeorm": "(rm ormconfig.json || :) && ts-node -r tsconfig-paths/register src/scripts/write-type-orm-config.ts",
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"typeorm:migration:generate": "npm run typeorm -- migration:generate -n",
"typeorm:migration:run": "npm run typeorm -- migration:run"

2. 創(chuàng)建遷移

現(xiàn)在我們可以運(yùn)行這個(gè)命令來創(chuàng)建一個(gè)初始化遷移:

npm run typeorm:migration:generate -- my_init

這會(huì)將 typeORM 連接到您的數(shù)據(jù)庫(kù)并生成一個(gè)數(shù)據(jù)庫(kù)遷移腳本 my_init.ts(在 typescript 中)并將其放入您項(xiàng)目的遷移文件夾中。
注意:您應(yīng)該將這些遷移腳本提交到您的源代碼管理中,并將這些文件視為只讀。
如果你想改變一些東西,想法是使用 CLI 命令在頂部添加另一個(gè)遷移。

3. 運(yùn)行遷移

npm run typeorm:migration:run

現(xiàn)在我們擁有了創(chuàng)建和運(yùn)行遷移所需的所有工具,而無需運(yùn)行 API 服務(wù)器項(xiàng)目,它在開發(fā)時(shí)為我們提供了很大的靈活性,我們可以隨時(shí)重新運(yùn)行、重新創(chuàng)建和添加它們。然而,在生產(chǎn)或階段環(huán)境中,您實(shí)際上經(jīng)常希望在部署之后 / 之后啟動(dòng) API 服務(wù)器之前自動(dòng)運(yùn)行遷移腳本。
為此,您只需添加一個(gè) start.sh 腳本即可。
您還可以添加一個(gè)環(huán)境變量 RUN_MIGRATIONS=<0|1> 來控制遷移是否應(yīng)該自動(dòng)運(yùn)行。

#!/bin/bash
設(shè)置 -e
設(shè)置 -x
如果 [ "$RUN_MIGRATIONS" ]; 然后
回顯“正在運(yùn)行的遷移”;
npm run typeorm:migration:run
fi
回聲“啟動(dòng)服務(wù)器”;
npm run start:prod

調(diào)試和數(shù)據(jù)庫(kù)工具

我們通過 API 完成同步數(shù)據(jù)庫(kù)字段工作 – 但我們的數(shù)據(jù)庫(kù)實(shí)際上反映了我們的數(shù)據(jù)模型嗎?
可以通過對(duì) DB 運(yùn)行一些 CLI 腳本查詢或使用 UI 數(shù)據(jù)庫(kù)管理工具進(jìn)行快速調(diào)試來檢查這一點(diǎn)。
使用 PostgreSQL 數(shù)據(jù)庫(kù)時(shí),我使用 pgAdmin
這是一個(gè)非常強(qiáng)大的工具,有一個(gè)漂亮的用戶界面。但是,我建議您使用以下工作流程:

我們現(xiàn)在可以看到表在數(shù)據(jù)庫(kù)中創(chuàng)建。1. 我們?cè)陧?xiàng)目中定義的項(xiàng)目表。2. 一個(gè)遷移表,在這個(gè)表中 typeORM 跟蹤已經(jīng)在這個(gè)數(shù)據(jù)庫(kù)上執(zhí)行了哪個(gè)遷移。(注意:您也應(yīng)該將此表視為只讀,否則 typeORM CLI 會(huì)混淆)

添加一些業(yè)務(wù)邏輯

現(xiàn)在讓我們添加一些業(yè)務(wù)邏輯。
為了演示,我將添加一個(gè)簡(jiǎn)單的 endpoint,它將返回表中的數(shù)據(jù)。
我們使用 Nest.js CLI 添加一個(gè)項(xiàng)目控制器和一個(gè)項(xiàng)目服務(wù)。

nest -- generate controller item
nest -- generate service item

這將為我們生成一些模板,然后我們添加:

// item.service.ts 

import { Injectable } from'@nestjs/common';
import { InjectRepository } from'@nestjs/typeorm';
import { Item } from'../model/item.entity';
import { Repository } from'typeorm';

@Injectable()
exportclass ItemService {
constructor(@InjectRepository(Item) private readonly repo: Repository<Item>) { }

publicasyncgetAll() {
returnawaitthis.repo.find();
}
}
// item.controller.ts

import { Controller, Get } from'@nestjs/common';
import { ItemService } from'./item.service';

@Controller('item')
exportclass ItemController {
constructor(private serv: ItemService) { }

@Get()
publicasyncgetAll() {
returnawaitthis.serv.getAll();
}
}

 然后通過 ItemModule 連接在一起,然后在 AppModule 中導(dǎo)入。

// item.module.ts

import { Module } from'@nestjs/common';
import { TypeOrmModule } from'@nestjs/typeorm';
import { ItemService } from'./item.service';
import { ItemController } from'./item.controller';
import { Item } from'../model/item.entity';

@Module({
imports: [TypeOrmModule.forFeature([Item])],
providers: [ItemService],
controllers: [ItemController],
exports: []
})
exportclass ItemModule { }

 啟動(dòng) API 后,curl 試試:

curl localhost:3000/item | jq
[] # << indicating no items in the DB - cool :)

不要暴露你的實(shí)體 —— 添加 DTO 和響應(yīng)

不要通過您的 API 向消費(fèi)者公開您在持久性上的實(shí)際數(shù)據(jù)模型。
當(dāng)你用一個(gè)數(shù)據(jù)傳輸對(duì)象包裝每個(gè)數(shù)據(jù)實(shí)體時(shí),你必須對(duì)它做序列化和反序列化。

在內(nèi)部數(shù)據(jù)模型(API 到數(shù)據(jù)庫(kù))和外部模型(API 消費(fèi)者到 API)之間應(yīng)該是有區(qū)別的。從長(zhǎng)遠(yuǎn)來看,這將幫助您解耦,令維護(hù)變得更容易。

// item.dto.ts

import { ApiModelProperty } from'@nestjs/swagger';
import { IsString, IsUUID, } from'class-validator';
import { Item } from'../model/item.entity';
import { User } from'../user.decorator';

exportclass ItemDTO implements Readonly<ItemDTO> {
@ApiModelProperty({ required: true })
@IsUUID()
id: string;

@ApiModelProperty({ required: true })
@IsString()
name: string;

@ApiModelProperty({ required: true })
@IsString()
description: string;

publicstaticfrom(dto: Partial<ItemDTO>) {
const it = new ItemDTO();
it.id = dto.id;
it.name = dto.name;
it.description = dto.description;
return it;
}

publicstaticfromEntity(entity: Item) {
returnthis.from({
id: entity.id,
name: entity.name,
description: entity.description
});
}

publictoEntity(user: User = null) {
const it = new Item();
it.id = this.id;
it.name = this.name;
it.description = this.description;
it.createDateTime = newDate();
it.createdBy = user ? user.id : null;
it.lastChangedBy = user ? user.id : null;
return it;
}
}

 現(xiàn)在我們可以像這樣簡(jiǎn)單地使用 DTO:

// item.controller.ts

@Get()
publicasync getAll(): Promise<ItemDTO[]> {
returnawaitthis.serv.getAll()
}

@Post()
publicasync post(@User() user: User, @Body() dto: ItemDTO): Promise<ItemDTO> {
returnthis.serv.create(dto, user);
}
// item.service.ts

publicasync getAll(): Promise<ItemDTO[]> {
returnawaitthis.repo.find()
.then(items => items.map(e => ItemDTO.fromEntity(e)));
}

publicasync create(dto: ItemDTO, user: User): Promise<ItemDTO> {
returnthis.repo.save(dto.toEntity(user))
.then(e => ItemDTO.fromEntity(e));
}

設(shè)置 OpenAPI(Swagger)
DTO 方法還使您能夠從它們生成 API 文檔(openAPI aka swagger docs)。您只需安裝:

npm install --save @nestjs/swagger swagger-ui-express

并在 main.ts 中添加這幾行

// main.ts

asyncfunction bootstrap() {
const app = await NestFactory.create(AppModule);

if (!configService.isProduction()) {

constdocument = SwaggerModule.createDocument(app, new DocumentBuilder()
.setTitle('Item API')
.setDescription('My Item API')
.build());

SwaggerModule.setup('docs', app, document);

}

await app.listen(3000);
}

本文章轉(zhuǎn)載微信公眾號(hào)@騰訊IMWeb前端團(tuán)隊(duì)

上一篇:

Go-Zero定義API實(shí)戰(zhàn):探索API語法規(guī)范與最佳實(shí)踐

下一篇:

使用REST API掌握Python:構(gòu)建強(qiáng)大Web服務(wù)的基本指南
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)