Salvo数据库集成实战:PostgreSQL与SQLx应用

Salvo数据库集成实战:PostgreSQL与SQLx应用

【免费下载链接】salvo A powerful web framework that can make your work easier 【免费下载链接】salvo 项目地址: https://gitcode.com/gh_mirrors/sa/salvo

引言:Rust Web开发的数据库痛点与解决方案

在现代Web开发中,数据库集成是构建动态应用的核心环节。Rust生态系统虽以内存安全和性能著称,但在数据库交互领域曾长期面临开发体验与性能难以兼顾的困境。Salvo作为新兴的Rust Web框架,结合SQLx的类型安全特性,为这一挑战提供了优雅的解决方案。

本文将通过实战案例,系统讲解如何在Salvo框架中集成PostgreSQL数据库,利用SQLx实现类型安全的数据访问。我们将从环境搭建、连接池配置、CRUD操作实现,到错误处理与性能优化,全面覆盖生产级应用开发的关键技术点。

读完本文后,你将能够:

  • 配置高性能的PostgreSQL连接池
  • 实现类型安全的数据库操作
  • 构建RESTful API处理数据库请求
  • 解决并发环境下的连接管理问题
  • 优化数据库交互性能

技术栈概览

组件版本要求作用
Salvo≥0.64.0Web框架核心
SQLx≥0.7.0数据库访问工具
PostgreSQL≥14关系型数据库
Tokio≥1.0异步运行时
Serde≥1.0数据序列化/反序列化

技术架构图

mermaid

环境准备与项目初始化

系统环境要求

  • Rust编译器:1.70.0或更高版本
  • PostgreSQL:14.x或更高版本
  • 开发工具:Git、Cargo-edit(可选)

项目创建与依赖配置

首先,创建一个新的Salvo项目并添加必要依赖:

# 创建项目目录
mkdir salvo-postgres-demo && cd salvo-postgres-demo

# 初始化Cargo项目
cargo init

# 添加核心依赖
cargo add salvo tokio --features full
cargo add sqlx --features postgres,runtime-tokio-native-tls,macros
cargo add serde --features derive
cargo add tracing tracing-subscriber
cargo add serde_json

Cargo.toml配置详解

最终的Cargo.toml文件应包含以下关键配置:

[package]
name = "salvo-postgres-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
salvo = "0.64"
tokio = { version = "1.0", features = ["full"] }
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls", "macros"] }
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = "0.3"
serde_json = "1.0"

PostgreSQL连接池配置

连接池核心概念

数据库连接池是管理数据库连接的关键组件,它通过复用已建立的连接来减少频繁创建和销毁连接的开销。在Salvo应用中,我们使用SQLx提供的PgPool来实现连接池管理。

全局连接池实现

use std::sync::OnceLock;
use sqlx::PgPool;

// 全局PostgreSQL连接池实例
static POSTGRES: OnceLock<PgPool> = OnceLock::new();

// 获取PostgreSQL连接池的辅助函数
#[inline]
pub fn get_postgres() -> &'static PgPool {
    POSTGRES.get().unwrap()
}

// 初始化连接池
async fn init_db() -> Result<(), sqlx::Error> {
    // 从环境变量或配置文件读取数据库URL
    let db_url = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://postgres:password@localhost/test".to_string());
    
    // 创建连接池
    let pool = PgPool::connect(&db_url).await?;
    
    // 存储连接池到全局变量
    if POSTGRES.set(pool).is_err() {
        tracing::warn!("数据库连接池已初始化");
    }
    
    Ok(())
}

连接池参数调优

生产环境中,需要根据服务器配置和预期负载调整连接池参数:

// 带参数配置的连接池初始化
let pool = PgPoolOptions::new()
    .max_connections(20) // 最大连接数
    .min_connections(5)  // 最小空闲连接数
    .acquire_timeout(Duration::from_secs(3)) // 获取连接超时时间
    .idle_timeout(Duration::from_secs(300)) // 连接空闲超时时间
    .connect(&db_url)
    .await?;

连接池配置建议

  • 最大连接数:通常设置为CPU核心数的2-4倍
  • 最小连接数:根据基础流量设置,避免频繁创建连接
  • 超时设置:根据网络环境和数据库响应时间调整

数据模型与类型安全

定义数据库模型

使用SQLx的FromRow宏可以轻松将数据库查询结果映射到Rust结构体:

use sqlx::FromRow;
use serde::Serialize;

// 用户模型 - 映射数据库表结构
#[derive(FromRow, Serialize, Debug, Clone)]
pub struct User {
    pub id: i64,
    pub username: String,
    pub email: Option<String>, // 可选字段
    pub created_at: chrono::DateTime<chrono::Utc>, // 时间类型
    pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
}

数据库迁移

SQLx提供了强大的迁移功能,可以管理数据库模式变更:

# 初始化迁移目录
sqlx migrate add init_users_table

# 编辑迁移文件(位于migrations/目录下)
# 应用迁移
sqlx migrate run

迁移文件示例(migrations/20230101000000_init_users_table.sql):

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP
);

-- 创建索引
CREATE INDEX idx_users_username ON users(username);

CRUD操作实现

创建数据(Create)

#[handler]
async fn create_user(req: &mut Request, res: &mut Response) -> Result<(), salvo::Error> {
    // 从请求体解析用户数据
    let new_user: CreateUserRequest = req.parse_json().await?;
    
    // 密码哈希处理
    let password_hash = bcrypt::hash(&new_user.password, 10)?;
    
    // 执行插入操作
    let result = sqlx::query_as!(
        User,
        r#"
        INSERT INTO users (username, email, password_hash)
        VALUES ($1, $2, $3)
        RETURNING id, username, email, created_at, updated_at
        "#,
        new_user.username,
        new_user.email,
        password_hash
    )
    .fetch_one(get_postgres())
    .await
    .map_err(|e| {
        tracing::error!("创建用户失败: {}", e);
        StatusCode::INTERNAL_SERVER_ERROR.with_reason("创建用户失败")
    })?;
    
    // 返回创建的用户信息
    res.render(Json(result));
    Ok(())
}

// 请求数据模型
#[derive(Deserialize)]
struct CreateUserRequest {
    username: String,
    email: Option<String>,
    password: String,
}

读取数据(Read)

实现一个支持分页和条件查询的用户列表接口:

#[handler]
async fn list_users(req: &mut Request, res: &mut Response) -> Result<(), salvo::Error> {
    // 从查询参数获取分页信息
    let page = req.query::<u32>("page").unwrap_or(1);
    let page_size = req.query::<u32>("page_size").unwrap_or(10);
    let offset = (page - 1) * page_size;
    
    // 可选的搜索参数
    let username = req.query::<String>("username");
    
    // 构建动态查询
    let mut query_builder = sqlx::query_as::<_, User>("SELECT * FROM users");
    let mut params = Vec::new();
    
    // 添加条件查询
    if let Some(username) = username {
        query_builder = query_builder.arg(format!("%{}%", username));
        query_builder = query_builder.sql(" WHERE username LIKE $1");
        params.push(1);
    }
    
    // 添加分页
    query_builder = query_builder.sql(&format!(
        " ORDER BY created_at DESC LIMIT ${} OFFSET ${}",
        params.len() + 1,
        params.len() + 2
    ));
    query_builder = query_builder.arg(page_size as i64);
    query_builder = query_builder.arg(offset as i64);
    
    // 执行查询
    let users = query_builder.fetch_all(get_postgres()).await?;
    
    // 获取总记录数用于分页
    let total = sqlx::query_scalar!(
        "SELECT COUNT(*) FROM users WHERE username LIKE $1",
        username.map(|u| format!("%{}%", u))
    )
    .fetch_one(get_postgres())
    .await?
    .unwrap_or(0);
    
    // 构建分页响应
    let response = PagedResponse {
        data: users,
        pagination: Pagination {
            page,
            page_size,
            total,
            total_pages: (total + page_size as i64 - 1) / page_size as i64,
        },
    };
    
    res.render(Json(response));
    Ok(())
}

// 分页响应结构
#[derive(Serialize)]
struct PagedResponse<T> {
    data: Vec<T>,
    pagination: Pagination,
}

#[derive(Serialize)]
struct Pagination {
    page: u32,
    page_size: u32,
    total: i64,
    total_pages: i64,
}

更新与删除操作(Update & Delete)

// 更新用户信息
#[handler]
async fn update_user(req: &mut Request, res: &mut Response) -> Result<(), salvo::Error> {
    let user_id = req.param("id").unwrap_or_default();
    let update_data: UpdateUserRequest = req.parse_json().await?;
    
    // 动态构建更新语句
    let mut updates = Vec::new();
    let mut params = Vec::new();
    let mut param_index = 1;
    
    if let Some(username) = &update_data.username {
        updates.push(format!("username = ${}", param_index));
        params.push(username.clone());
        param_index += 1;
    }
    
    if let Some(email) = &update_data.email {
        updates.push(format!("email = ${}", param_index));
        params.push(email.clone());
        param_index += 1;
    }
    
    if updates.is_empty() {
        return Err(StatusCode::BAD_REQUEST.with_reason("没有提供更新字段"));
    }
    
    // 添加更新时间
    updates.push(format!("updated_at = NOW()"));
    
    // 执行更新操作
    let query = format!(
        "UPDATE users SET {} WHERE id = ${}",
        updates.join(", "),
        param_index
    );
    
    let result = sqlx::query(&query)
        .bind_all(params)
        .bind(user_id)
        .execute(get_postgres())
        .await?;
    
    if result.rows_affected() == 0 {
        return Err(StatusCode::NOT_FOUND.with_reason("用户不存在"));
    }
    
    res.render(Json(ApiResponse {
        success: true,
        message: "用户信息已更新",
    }));
    Ok(())
}

// 删除用户
#[handler]
async fn delete_user(req: &mut Request, res: &mut Response) -> Result<(), salvo::Error> {
    let user_id = req.param("id").unwrap_or_default();
    
    let result = sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
        .execute(get_postgres())
        .await?;
    
    if result.rows_affected() == 0 {
        return Err(StatusCode::NOT_FOUND.with_reason("用户不存在"));
    }
    
    res.render(Json(ApiResponse {
        success: true,
        message: "用户已删除",
    }));
    Ok(())
}

错误处理与事务管理

全局错误处理

实现统一的错误处理机制,将数据库错误转换为HTTP响应:

// 自定义错误类型
#[derive(Debug)]
enum AppError {
    Database(sqlx::Error),
    Validation(String),
    Authentication(String),
    Authorization(String),
    NotFound(String),
}

// 实现错误转换
impl Into<salvo::Error> for AppError {
    fn into(self) -> salvo::Error {
        match self {
            AppError::Database(e) => {
                tracing::error!("数据库错误: {}", e);
                StatusCode::INTERNAL_SERVER_ERROR.with_reason("数据库操作失败")
            }
            AppError::Validation(msg) => {
                StatusCode::BAD_REQUEST.with_reason(msg)
            }
            AppError::Authentication(msg) => {
                StatusCode::UNAUTHORIZED.with_reason(msg)
            }
            AppError::Authorization(msg) => {
                StatusCode::FORBIDDEN.with_reason(msg)
            }
            AppError::NotFound(msg) => {
                StatusCode::NOT_FOUND.with_reason(msg)
            }
        }
    }
}

// 使用示例
#[handler]
async fn get_user(req: &mut Request) -> Result<User, AppError> {
    let user_id = req.param("id")
        .ok_or(AppError::Validation("用户ID参数缺失".to_string()))?;
    
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
        .fetch_one(get_postgres())
        .await
        .map_err(|e| match e {
            sqlx::Error::RowNotFound => AppError::NotFound(format!("用户ID {} 不存在", user_id)),
            _ => AppError::Database(e),
        })?;
    
    Ok(user)
}

事务管理

使用SQLx的事务支持确保数据一致性:

#[handler]
async fn transfer_funds(req: &mut Request) -> Result<(), AppError> {
    let transfer: TransferRequest = req.parse_json().await?;
    
    // 开始数据库事务
    let mut tx = get_postgres().begin().await.map_err(AppError::Database)?;
    
    // 检查源账户余额
    let source_balance = sqlx::query_scalar!(
        "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE",
        transfer.source_account_id
    )
    .fetch_one(&mut *tx)
    .await
    .map_err(AppError::Database)?
    .ok_or(AppError::NotFound("源账户不存在".to_string()))?;
    
    if source_balance < transfer.amount {
        return Err(AppError::Validation("余额不足".to_string()));
    }
    
    // 扣减源账户余额
    sqlx::query!(
        "UPDATE accounts SET balance = balance - $1 WHERE id = $2",
        transfer.amount,
        transfer.source_account_id
    )
    .execute(&mut *tx)
    .await
    .map_err(AppError::Database)?;
    
    // 增加目标账户余额
    sqlx::query!(
        "UPDATE accounts SET balance = balance + $1 WHERE id = $2",
        transfer.amount,
        transfer.target_account_id
    )
    .execute(&mut *tx)
    .await
    .map_err(AppError::Database)?;
    
    // 记录交易历史
    sqlx::query!(
        "INSERT INTO transactions (source_id, target_id, amount, timestamp) VALUES ($1, $2, $3, NOW())",
        transfer.source_account_id,
        transfer.target_account_id,
        transfer.amount
    )
    .execute(&mut *tx)
    .await
    .map_err(AppError::Database)?;
    
    // 提交事务
    tx.commit().await.map_err(AppError::Database)?;
    
    Ok(())
}

高级应用:数据库迁移与版本控制

集成SQLx迁移工具

Cargo.toml中添加SQLx CLI依赖:

[dev-dependencies]
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls", "macros", "cli"] }

创建和应用数据库迁移:

# 安装SQLx CLI(一次性)
cargo install sqlx-cli

# 初始化迁移目录
sqlx migrate init

# 创建新迁移
sqlx migrate add create_users_table

# 编辑迁移文件(位于migrations/目录)

# 应用迁移
sqlx migrate run

# 回滚最后一次迁移
sqlx migrate revert

迁移文件示例

-- migrations/20230901000000_create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP
);

-- 创建索引
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
-- migrations/20230902000000_add_user_status.sql
ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'active';

-- 创建枚举类型(可选)
CREATE TYPE user_status AS ENUM ('active', 'inactive', 'banned');
ALTER TABLE users ALTER COLUMN status TYPE user_status USING status::user_status;

性能优化策略

数据库查询优化

  1. 索引优化

    -- 为频繁查询的字段创建索引
    CREATE INDEX idx_orders_user_id ON orders(user_id);
    CREATE INDEX idx_orders_created_at ON orders(created_at);
    
    -- 复合索引用于多字段查询
    CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
    
  2. 查询优化

    // 避免SELECT *,只查询需要的字段
    let user = sqlx::query_as!(
        UserSummary,
        "SELECT id, username, email FROM users WHERE id = $1",
        user_id
    )
    .fetch_one(get_postgres())
    .await?;
    
    // 使用LIMIT分页,避免大量数据返回
    let recent_orders = sqlx::query_as!(Order, "SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC LIMIT 10", user_id)
        .fetch_all(get_postgres())
        .await?;
    

连接池监控

添加连接池监控中间件,跟踪连接使用情况:

#[handler]
async fn pool_stats(req: &mut Request, res: &mut Response) {
    let stats = get_postgres().metrics();
    
    res.render(Json(json!({
        "total_connections": stats.total_connections,
        "idle_connections": stats.idle_connections,
        "active_connections": stats.active_connections,
        "connections_waiting": stats.connections_waiting,
        "max_connections": stats.max_connections,
    })));
}

缓存策略

结合缓存减少数据库访问压力:

use lru::LruCache;
use std::sync::Mutex;

// 全局LRU缓存
static USER_CACHE: OnceLock<Mutex<LruCache<i64, User>>> = OnceLock::new();

#[handler]
async fn get_user_cached(req: &mut Request) -> Result<User, AppError> {
    let user_id = req.param("id").unwrap_or_default();
    
    // 尝试从缓存获取
    let mut cache = USER_CACHE.get_or_init(|| Mutex::new(LruCache::new(1000))).lock().unwrap();
    if let Some(user) = cache.get(&user_id) {
        return Ok(user.clone());
    }
    
    // 缓存未命中,从数据库获取
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
        .fetch_one(get_postgres())
        .await
        .map_err(|e| match e {
            sqlx::Error::RowNotFound => AppError::NotFound(format!("用户ID {} 不存在", user_id)),
            _ => AppError::Database(e),
        })?;
    
    // 存入缓存
    cache.put(user_id, user.clone());
    
    Ok(user)
}

完整示例:用户管理API

项目结构

salvo-postgres-demo/
├── src/
│   ├── main.rs          # 应用入口
│   ├── db/              # 数据库相关模块
│   │   ├── mod.rs       # 模块声明
│   │   ├── pool.rs      # 连接池管理
│   │   └── models.rs    # 数据模型定义
│   ├── handlers/        # 请求处理器
│   │   ├── mod.rs
│   │   ├── user.rs      # 用户相关接口
│   │   └── health.rs    # 健康检查接口
│   ├── routes.rs        # 路由定义
│   └── errors.rs        # 错误处理
├── migrations/          # SQLx迁移文件
├── Cargo.toml
└── .env                 # 环境变量配置

主程序入口

// src/main.rs
use salvo::prelude::*;
use tracing_subscriber::fmt::init;

mod db;
mod handlers;
mod routes;
mod errors;

#[tokio::main]
async fn main() {
    // 初始化日志
    init();
    
    // 初始化数据库连接池
    db::init_db().await.expect("数据库初始化失败");
    
    // 初始化缓存
    handlers::user::init_cache();
    
    // 创建路由
    let router = routes::create_router();
    
    // 启动服务器
    let acceptor = TcpListener::new("0.0.0.0:5800").bind().await;
    Server::new(acceptor).serve(router).await;
}

路由配置

// src/routes.rs
use salvo::prelude::*;
use super::handlers;

pub fn create_router() -> Router {
    Router::new()
        .push(Router::with_path("health").get(handlers::health::check))
        .push(Router::with_path("pool/stats").get(handlers::health::pool_stats))
        .push(Router::with_path("users")
            .get(handlers::user::list_users)
            .post(handlers::user::create_user))
        .push(Router::with_path("users/<id>")
            .get(handlers::user::get_user)
            .put(handlers::user::update_user)
            .delete(handlers::user::delete_user))
        .push(Router::with_path("transfers")
            .post(handlers::transfer::transfer_funds))
}

部署与监控

容器化部署

创建Dockerfile实现容器化部署:

FROM rust:1.70-slim as builder

WORKDIR /app

# 复制依赖文件
COPY Cargo.toml Cargo.lock ./
COPY src ./src
COPY migrations ./migrations

# 构建应用
RUN cargo build --release

# 生产镜像
FROM debian:bullseye-slim

WORKDIR /app

# 安装运行时依赖
RUN apt-get update && apt-get install -y libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/*

# 复制编译产物
COPY --from=builder /app/target/release/salvo-postgres-demo .
COPY --from=builder /app/migrations ./migrations

# 环境变量配置
ENV DATABASE_URL=postgres://postgres:password@db:5432/app
ENV RUST_LOG=info

# 暴露端口
EXPOSE 5800

# 启动命令
CMD ["./salvo-postgres-demo"]

监控与日志

集成Prometheus和Grafana监控应用性能:

// 添加Prometheus指标收集
use prometheus::{HistogramVec, IntCounterVec, Opts, Registry};

static DB_QUERY_DURATION: OnceLock<HistogramVec> = OnceLock::new();
static DB_QUERY_COUNT: OnceLock<IntCounterVec> = OnceLock::new();

// 初始化指标
fn init_metrics(registry: &Registry) {
    let query_duration = HistogramVec::new(
        Opts::new("db_query_duration_seconds", "Database query duration in seconds"),
        &["query_name", "status"],
    ).unwrap();
    
    let query_count = IntCounterVec::new(
        Opts::new("db_query_count", "Number of database queries"),
        &["query_name", "status"],
    ).unwrap();
    
    registry.register(Box::new(query_duration.clone())).unwrap();
    registry.register(Box::new(query_count.clone())).unwrap();
    
    DB_QUERY_DURATION.set(query_duration).unwrap();
    DB_QUERY_COUNT.set(query_count).unwrap();
}

// 使用指标包装数据库查询
async fn instrumented_query<F, R>(query_name: &str, f: F) -> Result<R, sqlx::Error>
where
    F: std::future::Future<Output = Result<R, sqlx::Error>>,
{
    let timer = DB_QUERY_DURATION.get().unwrap().with_label_values(&[query_name, "success"]).start_timer();
    
    match f.await {
        Ok(result) => {
            timer.observe_duration();
            DB_QUERY_COUNT.get().unwrap().with_label_values(&[query_name, "success"]).inc();
            Ok(result)
        }
        Err(e) => {
            DB_QUERY_DURATION.get().unwrap().with_label_values(&[query_name, "error"]).observe_duration();
            DB_QUERY_COUNT.get().unwrap().with_label_values(&[query_name, "error"]).inc();
            Err(e)
        }
    }
}

总结与最佳实践

关键知识点回顾

  1. 连接池管理

    • 使用OnceLock存储全局连接池
    • 根据应用需求调整连接池参数
    • 实现连接池监控,及时发现问题
  2. 类型安全

    • 使用FromRowquery_as!宏实现类型安全的数据访问
    • 定义清晰的数据模型和请求/响应结构
    • 利用编译时检查避免运行时错误
  3. 错误处理

    • 实现统一的错误处理机制
    • 区分不同类型的错误并返回适当的HTTP状态码
    • 记录详细的错误日志便于调试
  4. 性能优化

    • 使用索引优化查询性能
    • 实现查询结果缓存
    • 使用数据库事务确保数据一致性

生产环境检查清单

  •  连接池参数是否根据服务器配置优化
  •  是否实现了完善的错误处理和日志记录
  •  敏感数据是否加密存储(如密码哈希)
  •  是否使用参数化查询防止SQL注入
  •  数据库迁移是否版本化管理
  •  是否实现了性能监控和告警
  •  是否有适当的缓存策略减轻数据库负担
  •  并发操作是否正确使用事务

未来扩展方向

  1. 读写分离:实现主从复制,将读操作分流到从库
  2. 分库分表:使用Sharding技术扩展数据库容量
  3. ORM抽象:封装数据库访问层,便于切换数据库后端
  4. 数据访问层测试:实现完善的单元测试和集成测试
  5. 自动迁移:实现应用启动时的自动数据库迁移

通过本文介绍的方法,你已经掌握了在Salvo框架中集成PostgreSQL数据库的核心技术。这种类型安全的数据库访问方式不仅能提高代码质量,还能显著减少运行时错误,为构建可靠的Web应用奠定坚实基础。

随着Rust生态系统的不断成熟,Salvo和SQLx的组合将成为构建高性能、高可靠性Web应用的理想选择。建议继续关注这些项目的最新发展,及时应用新的特性和最佳实践。

【免费下载链接】salvo A powerful web framework that can make your work easier 【免费下载链接】salvo 项目地址: https://gitcode.com/gh_mirrors/sa/salvo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值