SpacetimeDB C++ 模块库实战:用 C++20 在数据库内构建 WebAssembly 模块
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
导读
SpacetimeDB C++ 模块库(crates/bindings-cpp)为 C++ 开发者提供了一套现代 C++20 API,用于编写编译为 WebAssembly 并在 SpacetimeDB 数据库内部运行的模块,从而将应用服务端逻辑直接下沉到数据库中,省去独立的业务服务层。本文围绕该库的官方 README 展开,完整覆盖其功能清单、架构设计、环境准备、快速上手、构建与发布流程、全套宏 API 参考,并结合仓库内的源码与示例模块(ARCHITECTURE.md、REFERENCE.md、modules/module-test-cpp/src/lib.cpp、modules/sdk-test-cpp/src/lib.cpp)做纵深讲解。读完本文,你将能够独立创建一个 C++ SpacetimeDB 模块:定义带约束的表、编写 reducer / view / procedure、构建出.wasm并发布到数据库,同时理解其底层类型注册与校验机制。
一、库定位与功能概览
SpacetimeDB C++ 模块库的核心定位是:用 C++20 编写运行在数据库内部的 WebAssembly 模块。与传统“数据库 → 应用服务器 → 客户端”的三层架构不同,模块直接在数据库内执行业务逻辑,客户端通过订阅实时获得数据同步。
该库提供了“生产就绪”的 C++ 绑定,覆盖了完整的类型系统支持,README 中列出的功能包括:
- 模块编译与发布:源码经 Emscripten 编译为 WASM 后发布到 SpacetimeDB;
- 全部生命周期 reducer:
init、client_connected、client_disconnected; - 用户自定义 reducer:支持不限数量的参数;
- 表注册与约束:
PrimaryKey、Unique、AutoInc; - 插入 / 更新 / 删除操作:基于类型安全的表访问器;
- 全部基础类型:
u8~u256、i8~i256、bool、f32、f64、string; - 全部特殊类型:
Identity、ConnectionId、Timestamp、TimeDuration、Uuid、Result<>; - 向量类型:所有基础类型与特殊类型均可构成
std::vector<T>; - 可选类型:
std::optional<T>; - 自定义结构体的 BSATN 序列化;
- 复杂枚举:支持带载荷的变体枚举及正确的变体命名;
- 增强日志系统:带文件 / 行号信息的多级别日志。
进阶能力
除基础功能外,README 还列出了可直接使用的高级特性:
| 特性 | 支撑宏 / API | 说明 |
|---|---|---|
| Btree 索引 | FIELD_Index | 配合range_from()、range_to()、range_inclusive()等实现优化查询 |
| 范围查询 | range_queries.h | 完整的区间查询体系 |
| 客户端可见性过滤 | SPACETIMEDB_CLIENT_VISIBILITY_FILTER | 行级安全,基于 SQL 谓词控制客户端可见行 |
| 定时 reducer | SPACETIMEDB_SCHEDULE | 基于ScheduleAt字段的时间驱动执行 |
| 过程(procedure) | SPACETIMEDB_PROCEDURE | 返回值的纯函数,可用显式事务访问数据库 |
| 视图(view) | SPACETIMEDB_VIEW | 只读查询函数,返回std::vector<T>或std::optional<T> |
| 字段访问器模式 | ctx.db[table_field] | 基于索引的高效操作 |
这些能力在仓库中均有可运行的示例:见modules/*-cpp/src/lib.cpp,其中 modules/module-test-cpp/src/lib.cpp 覆盖了索引、范围查询、枚举、调度、HTTP 处理器等用法,modules/sdk-test-cpp/src/lib.cpp 则是一个与 Rust / C# 测试模块完全等价的全量类型与操作测试模块(2091 行,覆盖每种基础类型、向量、可选、Result、唯一约束、主键表与调度表)。
二、架构设计:混合编译期 / 运行期体系
README 用四个要点概括了库的架构(详细技术文档见 crates/bindings-cpp/ARCHITECTURE.md):
- 混合编译期 / 运行期系统(Hybrid Compile-Time/Runtime System):C++20 concepts 在编译期完成校验,
__preinit__函数在 WASM 模块加载时执行运行期注册; - V9 类型注册系统:统一类型注册,具备全面的错误检测与循环引用防护;
- 名义类型系统(Nominal Type System):类型通过其声明的名字而非结构分析来标识,通过
SPACETIMEDB_STRUCT等宏显式注册; - 多层校验(Multi-Layer Validation):静态断言 → 运行期约束检查 → 错误模块替换策略,覆盖从编译到发布的全链路。
2.1 优先级有序的初始化系统
核心机制是带编号的__preinit__导出函数(实现于 crates/bindings-cpp/src/internal/Module.cpp),保证初始化顺序确定:
__preinit__01_ - 清理全局状态(最先执行) __preinit__10_ - 字段注册 __preinit__19_ - 自增集成与定时 reducer __preinit__20_ - 表与生命周期 reducer 注册 __preinit__21_ - 字段约束 __preinit__25_ - 行级安全过滤 __preinit__30_ - 用户 reducer __preinit__40_ - 视图 __preinit__50_ - 过程 __preinit__99_ - 类型校验与错误检测(最后执行)为什么需要编号?因为注册存在严格依赖:表必须先于约束存在,类型必须先于引用被注册,而校验必须发生在所有注册完成之后。WASM 线性内存模型要求确定性初始化,因此这种顺序是硬约束而非约定。
2.2 类型注册与循环引用防护
类型注册协调器是 V9Builder,但所有类型处理都委托给统一的ModuleTypeRegistration系统(crates/bindings-cpp/include/spacetimedb/internal/module_type_registration.h)。其核心原则是:只有用户自定义的结构体和枚举才进入类型空间(typespace),基础类型、数组、Optional 和特殊类型始终内联。注册流程依次检查:基础类型 → 数组 → Option → 特殊类型 → 用户自定义类型(注册并返回引用)。循环引用通过types_being_registered_集合跟踪,发现重复注册会立即设置全局错误标志并返回错误类型。
2.3 错误模块替换策略
在__preinit__99_validate_types中依次检查三类错误:循环引用(ERROR_CIRCULAR_REFERENCE_<type>)、多个主键(ERROR_MULTIPLE_PRIMARY_KEYS_<table>)、类型注册错误(ERROR_TYPE_REGISTRATION_<message>)。一旦命中,正常模块会被替换为包含无效类型引用的“错误模块”,SpacetimeDB 解析该类型时即失败,并把描述性错误名返回给开发者——这是从编译期到服务端的最后一道防线。
三、环境准备
构建 C++ 模块需要以下工具链:
| 依赖 | 版本要求 | 用途 |
|---|---|---|
| SpacetimeDB CLI | 最新版 | 初始化、构建、发布、调用 reducer、执行 SQL |
| Emscripten SDK (emsdk) | 最新版 | 将 C++ 编译为 WebAssembly |
| CMake | 3.16+(库的CMakeLists.txt声明最低 3.15) | 构建系统 |
| C++ 编译器 | 支持 C++20 | 编译源码 |
库本身的 CMake 配置见 crates/bindings-cpp/CMakeLists.txt:静态库目标spacetimedb_cpp_library(别名spacetimedb::spacetimedb_cpp_library),强制cxx_std_20;在 Emscripten 环境下会自动附加-O2 -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra编译选项,其中-fno-exceptions是 WASM 兼容性的关键——这也解释了为什么错误处理采用返回值而非异常。
四、快速上手
方式一:spacetime init(推荐)
# 创建一个新的 C++ 项目 spacetime init --lang cpp my-project cd my-project # 构建并发布 spacetime build -p ./spacetimedb spacetime publish -p ./spacetimedb my-databasespacetime init --lang cpp生成的项目结构为:
my-chat-module/spacetimedb/ ├── CMakeLists.txt ├── src/ └── lib.cpp └── .gitignore方式二:手动搭建
对已有项目,在 C++ 模块中加入以下代码即可。这是 README 给出的完整最小示例,涵盖了表、枚举、约束、reducer、生命周期 reducer、视图与过程:
#include <spacetimedb.h> using namespace SpacetimeDB; // 定义表结构 struct User { Identity identity; std::string name; std::string email; }; // 注册 BSATN 序列化 SPACETIMEDB_STRUCT(User, identity, name, email) // 注册为公共表 SPACETIMEDB_TABLE(User, users, Public) // 使用 FIELD_ 宏添加约束 FIELD_PrimaryKey(users, identity); FIELD_Unique(users, email); // 定义带命名空间限定的枚举 SPACETIMEDB_ENUM(UserRole, Admin, Moderator, Member) SPACETIMEDB_NAMESPACE(UserRole, "Auth") // 客户端代码中显示为 "Auth.UserRole" // 用户自定义 reducer SPACETIMEDB_REDUCER(add_user, ReducerContext ctx, std::string name, std::string email) { User user{ctx.sender(), name, email}; // id 将自动生成 ctx.db[users].insert(user); LOG_INFO("Added user: " + name); return Ok(); } // 按主键删除用户 SPACETIMEDB_REDUCER(delete_user, ReducerContext ctx) { ctx.db[users_identity].delete_by_key(ctx.sender()); return Ok(); } // 生命周期 reducer(可选) SPACETIMEDB_INIT(init, ReducerContext ctx) { LOG_INFO("Module initialized"); return Ok(); } SPACETIMEDB_CLIENT_CONNECTED(on_connect, ReducerContext ctx) { LOG_INFO("Client connected: " + ctx.sender().to_hex_string()); return Ok(); } SPACETIMEDB_CLIENT_DISCONNECTED(on_disconnect, ReducerContext ctx) { LOG_INFO("Client disconnected: " + ctx.sender().to_hex_string()); return Ok(); } // 定义视图:查询调用者自身的用户记录 SPACETIMEDB_VIEW(std::optional<User>, find_my_user, Public, ViewContext ctx) { // 使用索引字段按 identity 查找 return ctx.db[users_identity].find(ctx.sender()); } // 定义过程(带返回值的纯函数) SPACETIMEDB_PROCEDURE(uint32_t, add_numbers, ProcedureContext ctx, uint32_t a, uint32_t b) { return a + b; }4.1 手动搭建时的 CMake 配置
若不用spacetime init,参考 crates/bindings-cpp/REFERENCE.md 中的 CMake 配置(库的CMakeLists.txt也支持MODULE_SOURCE与OUTPUT_NAME两个缓存变量,默认分别为src/lib.cpp与lib):
cmake_minimum_required(VERSION 3.16) project(my-module) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 指向仓库中的 C++ 绑定目录 set(SPACETIMEDB_CPP_LIBRARY_PATH "path/to/crates/bindings-cpp") add_executable(lib src/lib.cpp) target_include_directories(lib PRIVATE ${SPACETIMEDB_CPP_LIBRARY_PATH}/include) add_subdirectory(${SPACETIMEDB_CPP_LIBRARY_PATH} spacetimedb_cpp_library) target_link_libraries(lib PRIVATE spacetimedb_cpp_library) # Emscripten 下的 WASM 设置 if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") set_target_properties(lib PROPERTIES SUFFIX ".wasm" LINK_FLAGS "-s STANDALONE_WASM=1 ..." ) endif()头文件入口为单头文件 crates/bindings-cpp/include/spacetimedb.h,其中按模块系统、表与约束、reducer、procedure、视图等分组聚合了全部子头文件。
五、构建与发布模块
构建步骤
# 进入模块目录 cd modules/your-module # 构建项目 spacetime build -p ./spacetimedb # 发布到 SpacetimeDB(显式指定 wasm 路径) spacetime publish --bin-path ./spacetimedb/build/lib.wasm your-database-name # 或直接给目录(自动检测 build/lib.wasm) spacetime publish ./spacetimedb your-database-name自定义模块源码
若要构建不同的源文件,可覆盖 CMake 变量(仓库的编译测试即采用此方式,参考 crates/bindings-cpp/tests/compile/run-compile-tests.sh):
# 构建指定的测试模块 emcmake cmake -B build -DMODULE_SOURCE=src/test_module.cpp -DOUTPUT_NAME=test_module . cmake --build build # 生成 build/test_module.wasm完整的手动构建 / 发布命令
# 使用 spacetime spacetime build -p . # 手动构建 emcmake cmake -B build . cmake --build build # 发布 spacetime publish . my-database # 或手动指定产物 spacetime publish --bin-path build/lib.wasm my-database启动本地实例后可先用 CLI 验证模块行为(来自 crates/bindings-cpp/QUICKSTART.md):
spacetime start # 启动本地 SpacetimeDB spacetime call my-db set_name "Alice" # 调用 reducer spacetime sql my-db "SELECT * FROM user" # 查询数据六、宏 API 参考
表定义
| 宏 | 说明 |
|---|---|
SPACETIMEDB_TABLE(Type, table_name, Public/Private) | 注册一张表 |
SPACETIMEDB_STRUCT(Type, field1, field2, ...) | 为类型注册 BSATN 序列化 |
可见性规则:Public 表自动同步给订阅客户端;Private 表仅 reducer 可访问、不同步给客户端。同一个结构体可注册为多张表(例如一张私有错误日志表加一张公共审计日志表)。
枚举定义
| 宏 | 说明 |
|---|---|
SPACETIMEDB_ENUM(EnumName, Value1, Value2, ...) | 定义简单枚举(单元变体) |
SPACETIMEDB_ENUM(EnumName, (Variant1, Type1), (Variant2, Type2), ...) | 定义带载荷的变体枚举 |
SPACETIMEDB_NAMESPACE(EnumName, "Namespace") | 为枚举添加命名空间限定 |
变体枚举底层是std::variant,因此每个载荷类型必须唯一——需要多个单元变体时,用SPACETIMEDB_UNIT_TYPE(Name)创建唯一的空类型(参见 modules/module-test-cpp/src/lib.cpp 中TestFFoo/TestFBar的做法,这是与 C# SDK 直接复用Unit的差异点)。
Reducer
| 宏 | 说明 |
|---|---|
SPACETIMEDB_REDUCER(name, ReducerContext ctx, ...) | 用户自定义 reducer |
SPACETIMEDB_INIT(name, ReducerContext ctx) | 模块初始化 reducer(可选) |
SPACETIMEDB_CLIENT_CONNECTED(name, ReducerContext ctx) | 客户端连接 reducer(可选) |
SPACETIMEDB_CLIENT_DISCONNECTED(name, ReducerContext ctx) | 客户端断开 reducer(可选) |
Reducer 的关键语义:
- 返回
ReducerResult(即Outcome<void>的类型别名); - 成功用
return Ok();,失败用return Err("message");; - 返回
Err会触发整个事务回滚,错误消息序列化后返回调用方,不会导致 WASM 崩溃; - 第一个参数必须是
ReducerContext ctx,其余参数由客户端传入且必须已注册序列化。
ReducerContext提供的能力包括:ctx.sender()(调用方身份)、ctx.timestamp(当前时间戳)、ctx.rng()(确定性随机数,以 reducer 时间戳微秒为种子)、ctx.database_identity()、ctx.sender_auth()(JWT 鉴权)以及ctx.db[...]数据库访问。
视图(View)
| 宏 | 说明 |
|---|---|
SPACETIMEDB_VIEW(return_type, name, Public/Private, ViewContext ctx) | 只读查询函数 |
SPACETIMEDB_VIEW(return_type, name, Public/Private, AnonymousViewContext ctx) | 匿名视图(无发送方身份) |
注意:视图当前只支持 context 参数,尚不支持额外的调用参数。
过程(Procedure)
| 宏 | 说明 |
|---|---|
SPACETIMEDB_PROCEDURE(return_type, name, ProcedureContext ctx, ...) | 返回值的纯函数 |
要点:
- 直接返回类型本身(不包裹在
Outcome中),可为任意 SpacetimeType(基础类型、结构体、枚举、Unit等); - 访问数据库需要显式事务:
ctx.WithTx()或ctx.TryWithTx(); - 始终公开,无访问控制。
字段约束(在表注册后应用)
| 宏 | 说明 |
|---|---|
FIELD_PrimaryKey(table_name, field) | 主键约束 |
FIELD_PrimaryKeyAutoInc(table_name, field) | 自增主键 |
FIELD_Unique(table_name, field) | 唯一约束 |
FIELD_UniqueAutoInc(table_name, field) | 自增唯一字段 |
FIELD_Index(table_name, field) | 索引(btree),加速查询与范围操作 |
FIELD_IndexAutoInc(table_name, field) | 自增索引字段 |
FIELD_AutoInc(table_name, field) | 仅自增,无其他约束 |
FIELD_NamedMultiColumnIndex(table, index_name, col1, col2) | 多列 btree 索引 |
FIELD_Default(table, field, value) | 字段默认值(迁移 / 加列场景) |
约束的合法类型有硬性要求(见 crates/bindings-cpp/REFERENCE.md):
| 约束类型 | 允许的类型 |
|---|---|
| PrimaryKey | 整数、bool、string、Identity、ConnectionId、Timestamp、枚举 |
| Unique | 同 PrimaryKey |
| Index | 同 PrimaryKey |
| AutoInc | 仅整数类型 |
这些限制在编译期由 C++20 concepts(如FilterableValue、AutoIncrementable,定义于 crates/bindings-cpp/include/spacetimedb/table_with_constraints.h)和static_assert强制执行,违反会得到带字段名和指引的清晰编译错误。
自增回调机制
使用自增字段时,insert()会自动返回带生成 ID 的行对象。底层流程(ARCHITECTURE.md 有完整描述):insert()序列化并发送行 → 服务端生成自增值 → 服务端仅回传生成的列值(BSATN 格式)→ SDK 调用注册在__preinit__19_的集成函数把生成值写回原行 →insert()返回填充完整的行。因此插入后立即可用生成 ID:
User user{0, "Bob", true}; // id=0 是占位符,将被自动生成 User inserted = ctx.db[user].insert(user); LOG_INFO("Created user with ID: " + std::to_string(inserted.id));支持多个自增字段共存(例如FIELD_PrimaryKeyAutoInc与FIELD_UniqueAutoInc同时存在时,所有生成值都会被集成)。
七、日志系统
LOG_DEBUG("Debug message"); LOG_INFO("Info message"); LOG_WARN("Warning message"); LOG_ERROR("Error message"); LOG_PANIC("Fatal error message"); // 带计时 { LogStopwatch timer("Operation name"); // ... 需要计时的代码 ... } // 离开作用域时自动输出耗时实现位于 crates/bindings-cpp/include/spacetimedb/logger.h,日志包含文件 / 行号等源码位置信息。日志级别还可通过模块源码顶部的#define STDB_LOG_LEVEL覆盖(modules/module-test-cpp/src/lib.cpp 中即设置为TRACE)。
八、数据库访问模式详解
C++ 绑定使用独特的双访问器模式(QUICKSTART.md 称之为 “unique accessor pattern”):
ctx.db[tableName]—— 表访问,用于迭代和基础操作,如ctx.db[user].insert(...)、for (const auto& row : ctx.db[user])、ctx.db[user].count();ctx.db[tableName_fieldName]—— 字段访问器,用于基于索引的高效操作,如ctx.db[user_identity].find(...)、delete_by_key(...)、filter(...)。
| 操作 | 表访问 | 字段访问(索引) |
|---|---|---|
| Insert | insert(row) | — |
| Delete | 手动迭代 | delete_by_key(key) |
| Update | update(row) | update(row) |
| 查询 | 迭代 | filter(value) |
范围查询
对已建FIELD_Index的字段可进行范围查询(头文件 crates/bindings-cpp/include/spacetimedb/range_queries.h):
auto range1 = range_from(25); // 25.. (>= 25) auto range2 = range_to(30); // ..30 (< 30) auto range3 = range(20, 35); // 20..35(>= 20, < 35) auto range4 = range_inclusive(20, 35); // 20..=35(>= 20, <= 35) auto range5 = range_to_inclusive(30); // ..=30 auto range6 = range_full<int>(); // 全量 bool in_range = range4.contains(25); // true // 对索引字段过滤,字符串区间同样可用 for (const auto& product : ctx.db[product_item_price].filter(price_range)) { LOG_INFO("Product in range: " + product.name); }modules/module-test-cpp/src/lib.cpp 的test_btree_index_argsreducer 对整数、多列坐标、字符串三类区间做了完整验证,并对比了“基于索引的范围过滤”与“全表手动过滤”的结果一致性。
九、类型系统细节
基础类型与容器
C++ 绑定支持全部标准整数 / 浮点类型,外加 SpacetimeDB 专属的大整数类型u128、u256、i128、i256,以及std::string、std::vector<T>、std::optional<T>、Result<T, E>。注意类型必须精确匹配(例如用uint32_t而非int,QUICKSTART.md 的故障排查一节特别强调了这一点)。
命名空间限定系统
SPACETIMEDB_NAMESPACE(EnumName, "Prefix")是一个纯编译期特性:它通过模板特化SpacetimeDB::detail::namespace_info<T>存储常量字符串,LazyTypeRegistrar在注册时用if constexpr检测命名空间并拼接限定名(如Auth.UserRole)。客户端代码生成器据此在 TypeScript、C#、Rust 客户端中组织类型,而服务端 C++ 代码仍使用未限定的名字。优点是零运行期开销、可选且向后兼容(详见 crates/bindings-cpp/ARCHITECTURE.md 的命名空间章节)。
十、已知限制
README 明确列出了当前版本的边界:
类型系统
- 非常大的类型组合可能超过 WASM 内存限制;
- 复杂递归类型引用需要仔细安排注册顺序。
数据库操作
- 基于索引的操作使用字段访问器:
ctx.db[table_field].delete_by_key(value); - 表约束由服务端声明并强制实施;
- 通过字段访问器支持 insert / delete / update。
- 基于索引的操作使用字段访问器:
高级特性
FIELD_Index创建 btree 索引以支持高效范围查询;- 支持
range_from()、range_to()、range_inclusive()等完整范围查询体系; - 行级安全通过
SPACETIMEDB_CLIENT_VISIBILITY_FILTER实现; - 迁移能力有限:仅支持自动添加表;
- SQL 执行:仅能通过 CLI(
spacetime sql)使用,模块内部不可执行 SQL。
此外,ARCHITECTURE.md 也提示其内容中仍保留少量历史实现描述,当前实现已精简为生产就绪状态。
十一、示例模块与测试
README 指向modules/*-cpp/src/目录下的示例:
- modules/module-test-cpp/src/lib.cpp —— 与 Rust
module-test等价:约束 / 索引 / 枚举 / 视图 / 调度 reducer / JWT 鉴权 / 过程 / HTTP 处理器全覆盖,并包含大量范围查询验证; - modules/sdk-test-cpp/src/lib.cpp —— 全量类型与操作测试模块,与 Rust、C# SDK 测试模块完全等价:每种基础类型与特殊类型的单值表、向量表、可选表、Result 表、唯一约束表、主键表,以及对应的 insert / delete / update reducer。
测试体系方面,仓库还提供了:
- 类型隔离测试:crates/bindings-cpp/tests/type-isolation-test/(含
error_circular_ref.cpp、error_multiple_pk.cpp、error_autoinc_non_integer.cpp等负向用例与module01~module12的正向用例); - 编译期校验测试:crates/bindings-cpp/tests/compile/cases/;
- 查询构建器编译与 SQL 测试:crates/bindings-cpp/tests/query-builder-compile/、crates/bindings-cpp/tests/query-builder-sql/。
这些用例从编译期概念校验、运行期注册校验到 SQL 查询语义多个层面印证了本文所述的架构设计。
十二、常见问题排查
QUICKSTART.md 给出的排查要点:
- 构建错误:确保 Emscripten SDK 为最新,并使用
emcmake cmake; - 模块找不到:检查 SpacetimeDB 是否正在运行;
- 类型错误:C++ 类型必须精确匹配,用
uint32_t而不是int; - 约束冲突:约束由数据库强制执行,主键重复会使 reducer 失败(返回
Err触发回滚)。
结语
SpacetimeDB C++ 模块库以“编译期概念校验 + 运行期__preinit__注册 + 名义类型系统 + 多层错误检测”的混合架构,在 WASM 环境约束(无异常、16MB 初始内存、线性初始化)下提供了接近 Rust 绑定体验的类型安全开发流程。配合仓库内的完整示例与测试模块,开发者可以从一个最小聊天模块起步,逐步掌握表约束、范围查询、行级安全、定时执行与过程调用等全部能力,将 C++ 后端逻辑直接搬进数据库内部运行。
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考