docs: 更新 README——注解(@SimApiSign/@AesBody)、stdx.http、simapi_serialization 依赖、字段无下划线

- annotations 子包(原 attributes)
- @SimApiSign / @AesBody 声明式注解(替代手动调用)
- SimApiSignProviderBase / AesBodyProviderBase 移至 interfaces
- SimApiHttpClient/SimApiStorage/verifySign 基于 stdx.net.http(不依赖 soulsoft_net_http)
- SimApiUtil.json/escapeJson 直接实现、fromJson 免约束
- 依赖表:去掉 soulsoft_serialization/soulsoft_net_http,加 simapi_serialization
This commit is contained in:
2026-08-18 02:08:00 +08:00
parent aa2af7aee1
commit 11360b3359
+54 -53
View File
@@ -2,7 +2,7 @@
> 仓颉版 SimApiASP.NET Core 风格 API 基础框架,移植自 C# 项目 [SimApi](https://github.com/SimcuTeam/simapi-net)`E:\simcu\simapi-net`)。 > 仓颉版 SimApiASP.NET Core 风格 API 基础框架,移植自 C# 项目 [SimApi](https://github.com/SimcuTeam/simapi-net)`E:\simcu\simapi-net`)。
提供**统一响应格式、异常拦截、Token 认证、缓存、工具集、HTTP 客户端**等 API 基础能力。 提供**统一响应格式、异常拦截、Token 认证、缓存、工具集、HTTP 客户端、S3 存储、声明式注解**等 API 基础能力。
--- ---
@@ -18,6 +18,7 @@ import soulsoft_extensions_logging.*
import soulsoft_extensions_injection.* import soulsoft_extensions_injection.*
import simapi.* import simapi.*
import simapi.communications.* import simapi.communications.*
import simapi.helpers.*
main(args: Array<String>) { main(args: Array<String>) {
let builder = WebHost.createBuilder(args) let builder = WebHost.createBuilder(args)
@@ -34,10 +35,10 @@ main(args: Array<String>) {
let host = builder.build() let host = builder.build()
SimApiExtensions.useSimApi(host) SimApiExtensions.useSimApi(host)
// 业务接口:返回统一响应格式 // 业务接口:返回对象自动封装为统一响应格式({code, message, data}
host.mapGet("hello") { host.mapGet("hello") {
context => context =>
context.response.write(SimApiBaseResponse().toJsonString(dataJson: "\"hello cangjie.\"")) context.response.write(SimApiUtil.json(Some(SimApiResponse<String>("hello cangjie."))))
} }
host.run() host.run()
@@ -58,6 +59,12 @@ main(args: Array<String>) {
| 404 | 资源不存在 | | 404 | 资源不存在 |
| 500 | 服务器错误 | | 500 | 服务器错误 |
响应 JSON(经 simapi_serialization 反射序列化,字段**无下划线**):
```json
{ "code": 200, "message": "成功", "data": { ... } }
```
### 异常处理流程 ### 异常处理流程
``` ```
@@ -75,7 +82,8 @@ simapi-cj/
├── cjpm.toml # 包配置 ├── cjpm.toml # 包配置
├── src/ ├── src/
│ ├── SimApiExtensions.cj # 根包入口:SimApiExtensions 静态类(addSimApi / useSimApi + 内置路由 + 响应封装) │ ├── SimApiExtensions.cj # 根包入口:SimApiExtensions 静态类(addSimApi / useSimApi + 内置路由 + 响应封装)
│ ├── attributes/ # 声明式注解:@SimApiAuth(鉴权)、@OriginResponse(原样响应) │ ├── annotations/ # 声明式注解:@SimApiAuth(鉴权)、@OriginResponse(原样响应)
│ │ # @SimApiSign(验签)、@AesBodyAES body 解密)
│ ├── authsdk/ # 认证中心 SDKSimApiAuthClient/Center/Iam + 网关中间件 + DTO │ ├── authsdk/ # 认证中心 SDKSimApiAuthClient/Center/Iam + 网关中间件 + DTO
│ ├── communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, 请求 DTO │ ├── communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, 请求 DTO
│ ├── configurations/ # SimApiOptions + 各模块 Option(含 ConfigureSimApiXxx 回调) │ ├── configurations/ # SimApiOptions + 各模块 Option(含 ConfigureSimApiXxx 回调)
@@ -84,7 +92,7 @@ simapi-cj/
│ ├── helpers/ # SimApiError, SimApiUtil, SimApiAuth, SimApiCache, SimApiHttpClient, │ ├── helpers/ # SimApiError, SimApiUtil, SimApiAuth, SimApiCache, SimApiHttpClient,
│ │ # SimApiAesUtil(AES-256), SimApiSignChecker(验签), SimApiAesBodyChecker(AES body), │ │ # SimApiAesUtil(AES-256), SimApiSignChecker(验签), SimApiAesBodyChecker(AES body),
│ │ # SimApiStorage(S3/MinIO, 自实现 SigV4), SimApiRequestDelegateFactory, SimApiResultWriter │ │ # SimApiStorage(S3/MinIO, 自实现 SigV4), SimApiRequestDelegateFactory, SimApiResultWriter
│ ├── interfaces/ # ISimApiAuthChecker, IBindRequestContext │ ├── interfaces/ # ISimApiAuthChecker, IBindRequestContext, SimApiSignProviderBase, AesBodyProviderBase
│ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志) │ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志)
│ ├── macros/ # ReadTomlVersion(编译期读版本号) │ ├── macros/ # ReadTomlVersion(编译期读版本号)
│ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware │ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware
@@ -128,12 +136,12 @@ auth.logoutAll("user-001") // 退出全部
- **InMemory 模式**:零配置,适合开发/测试;登录态带过期时间(对齐 C# 过期语义),重启后丢失 - **InMemory 模式**:零配置,适合开发/测试;登录态带过期时间(对齐 C# 过期语义),重启后丢失
- **Token 传参**Header `Token: <value>` 或 Query `token=<value>` - **Token 传参**Header `Token: <value>` 或 Query `token=<value>`
### 2.1 声明式鉴权 — @SimApiAuth注解类,对齐 C# [SimApiAuth] ### 2.1 声明式鉴权 — @SimApiAuth(对齐 C# [SimApiAuth]
标注在控制器**方法或类**上,请求派发时自动执行鉴权(未登录 401 → 类型不匹配 403 → 遍历执行 `ISimApiAuthChecker`,替代手动 `requireLogin()` 标注在控制器**方法或类**上,请求派发时自动执行鉴权(未登录 401 → 类型不匹配 403 → 遍历执行 `ISimApiAuthChecker`):
```cangjie ```cangjie
import simapi.attributes.{SimApiAuth} import simapi.annotations.{SimApiAuth}
@SimApiAuth // 类级:整个控制器需登录 @SimApiAuth // 类级:整个控制器需登录
public class MyController <: SimApiBaseController { public class MyController <: SimApiBaseController {
@@ -144,14 +152,14 @@ public class MyController <: SimApiBaseController {
} }
``` ```
> 说明:仓颉注解参数须为编译期常量,`@SimApiAuth` 支持单个类型参数(`@SimApiAuth["admin"]`)或逗号分隔多类型(`@SimApiAuth["admin,user"]`,对齐 C# `type.Split(",")`);空参数表示任意已登录用户。多个 `ISimApiAuthChecker` 通过 `SimApiOptions.authCheckers` 注册(由 addSimApi 扫描调用者包填充)。 > 说明:仓颉注解参数须为编译期常量,`@SimApiAuth` 支持单个类型参数(`@SimApiAuth["admin"]`)或逗号分隔多类型(`@SimApiAuth["admin,user"]`,对齐 C# `type.Split(",")`);空参数表示任意已登录用户。
### 2.2 原样响应 — @OriginResponse ### 2.2 原样响应 — @OriginResponse
标注后跳过统一响应封装,接口返回什么就输出什么(对齐 C# `[OriginResponse]`): 标注后跳过统一响应封装,接口返回什么就输出什么(对齐 C# `[OriginResponse]`):
```cangjie ```cangjie
import simapi.attributes.{OriginResponse} import simapi.annotations.{OriginResponse}
@OriginResponse @OriginResponse
@HttpGet["raw"] @HttpGet["raw"]
@@ -160,36 +168,35 @@ public func raw(): String {
} }
``` ```
### 2.3 服务端验签 — SimApiSignChecker(对齐 C# [SimApiSign] ### 2.3 声明式验签 — @SimApiSign(对齐 C# [SimApiSign]
校验带签名请求(appId 提取 → 密钥获取 → timestamp 过期校验 → nonce 去重 → MD5 比对): 标注在控制器**方法或类**上,请求派发时自动验签(appId 提取 → 密钥获取 → timestamp 过期校验 → nonce 去重 → MD5 比对):
```cangjie ```cangjie
import simapi.helpers.{SimApiSignProviderBase, SimApiSignChecker} import simapi.annotations.{SimApiSign}
import simapi.interfaces.{SimApiSignProviderBase}
// 1. 继承 Provider 实现密钥获取 // 1. 继承 Provider 实现密钥获取(并注册到 DI
public class MySignProvider <: SimApiSignProviderBase { public class MySignProvider <: SimApiSignProviderBase {
public override func getKey(appId: ?String): ?String { public override func getKey(appId: ?String): ?String {
// 根据 appId 返回密钥(如查库)
Some("my-secret-key") Some("my-secret-key")
} }
} }
// 2. 控制器方法开头调用校验 // 2. 方法标注 @SimApiSign,自动验签(provider 类型名从 DI 解析)
public func signedAction(): String { @SimApiSign["MySignProvider"]
SimApiSignChecker.verify(context, provider, cache) public func signedAction(): String { "ok" }
"ok"
}
``` ```
Provider 可配置:`appIdName` / `timestampName` / `nonceName` / `signName` / `queryExpires` / `duplicateRequestProtection` / `signFields`(与 C# `SimApiSignProviderBase` 一致) `SimApiSignProviderBase` 可配置:`appIdName` / `timestampName` / `nonceName` / `signName` / `queryExpires` / `duplicateRequestProtection` / `signFields`(与 C# 一致)。也可手动调用 `SimApiSignChecker.verify(context, provider, cache)`
### 2.4 AES body 解密 — SimApiAesBodyChecker(对齐 C# [AesBody] ### 2.4 声明式 AES body — @AesBody(对齐 C# [AesBody]
服务端接收 `{"data":"密文"}` 加密 body,解密后返回明文 JSON(控制器再反序列化为目标类型 标注在**参数**上,请求派发时自动解密 `{"data":"密文"}` body反序列化为参数类型:
```cangjie ```cangjie
import simapi.helpers.{AesBodyProviderBase, SimApiAesBodyChecker} import simapi.annotations.{AesBody}
import simapi.interfaces.{AesBodyProviderBase}
public class MyAesProvider <: AesBodyProviderBase { public class MyAesProvider <: AesBodyProviderBase {
public override func getKey(appId: ?String): ?String { public override func getKey(appId: ?String): ?String {
@@ -197,13 +204,14 @@ public class MyAesProvider <: AesBodyProviderBase {
} }
} }
public func create(@FromBody req: AesBodyRequest): String { public func create(@AesBody["MyAesProvider"] request: CreateRequest): String {
let json = SimApiAesBodyChecker.decryptBody(context, provider) // 解密后的 JSON 字符串 // request 已自动解密并反序列化
let dto = JsonSerializer.deserializeObject<MyDto>(json)
"ok" "ok"
} }
``` ```
也可手动调用 `SimApiAesBodyChecker.decryptBody(context, provider)` 获取明文 JSON 字符串。
### 3. 缓存 — SimApiCache ### 3. 缓存 — SimApiCache
```cangjie ```cangjie
@@ -227,12 +235,16 @@ SimApiUtil.md5("text") // 32 位十六进制
SimApiUtil.sha1("text") // 40 位 SimApiUtil.sha1("text") // 40 位
SimApiUtil.base64Encode("text") / base64Decode("...") SimApiUtil.base64Encode("text") / base64Decode("...")
SimApiUtil.base64Encode(obj) // 对象 → JSON → Base64(对齐 C# Base64Encode(object) SimApiUtil.base64Encode(obj) // 对象 → JSON → Base64(对齐 C# Base64Encode(object)
SimApiUtil.fromJson<T>(json) // JSON → T(对齐 C# FromJson<T>T 需 ISerialization<T> SimApiUtil.json(obj) // 对象 → JSON 字符串(simapi_serialization 反射
SimApiUtil.base64DecodeTo<T>(str) // Base64 → JSON → T(对齐 C# Base64Decode<T> SimApiUtil.escapeJson(s) // JSON 字符串转义
SimApiUtil.fromJson<T>(json) // JSON → T(对齐 C# FromJson<T>,任意类免约束)
SimApiUtil.base64DecodeTo<T>(str) // Base64 → JSON → T
SimApiUtil.checkCell("13800138000") // 手机号 SimApiUtil.checkCell("13800138000") // 手机号
SimApiUtil.checkEmail("a@b.com") // 邮箱 SimApiUtil.checkEmail("a@b.com") // 邮箱
``` ```
> JSON 序列化/反序列化统一走 **simapi_serialization**`JsonSerializer.Serialize` / `Deserialize<T>`),任意类免标注、免接口约束。
### 4.1 AES 加解密 — SimApiAesUtil(对齐 C# SimApiAesUtil ### 4.1 AES 加解密 — SimApiAesUtil(对齐 C# SimApiAesUtil
纯仓颉实现 AES-256-CBC + PKCS7S-box/密钥扩展/轮函数),与 .NET 双向互操作已验证: 纯仓颉实现 AES-256-CBC + PKCS7S-box/密钥扩展/轮函数),与 .NET 双向互操作已验证:
@@ -262,25 +274,24 @@ user.updateTime() // 刷新 _updatedAt
### 5. HTTP 客户端 — SimApiHttpClient ### 5. HTTP 客户端 — SimApiHttpClient
用于调用其他带签名/AES 的 SimApi 服务(**内置 TLS 支持**`https` 自动配置信任所有证书 + SNI,仓颉生态下 stdx TLS 动态加载 openssl 可用): 用于调用其他带签名/AES 的 SimApi 服务(**基于 stdx.net.http,不依赖 soulsoft_net_http**内置 TLS`https` 自动配置信任所有证书 + SNI):
```cangjie ```cangjie
let client = SimApiHttpClient(options: SimApiHttpClientOptions()) // 配置 server/appId/appKey let client = SimApiHttpClient(options: SimApiHttpClientOptions()) // 配置 server/appId/appKey
// 返回泛型 T(对齐 .NET SignQuery<T>/AesQuery<T>/AesSignQuery<T>),T 需实现 ISerialization<T> // 返回泛型 T(对齐 .NET SignQuery<T>/AesQuery<T>/AesSignQuery<T>),T 任意类免约束
let resp1 = client.signQuery<SimApiLoginItem>("/api/hello", body: "{\"a\":1}") let resp1 = client.signQuery<SimApiLoginItem>("/api/hello", body: "{\"a\":1}")
let resp2 = client.aesQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}") let resp2 = client.aesQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}")
let resp3 = client.aesSignQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}") let resp3 = client.aesSignQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}")
``` ```
签名参数名可配置(`SimApiHttpClient` 实例属性 `signName / timestampName / nonceName / appIdName / signFields`,对齐 C# 的 virtual 属性)。 签名参数名可配置(`signName / timestampName / nonceName / appIdName / signFields`,对齐 C# 的 virtual 属性)。AES 请求体用 `SimApiOneFieldRequest<String>` 序列化为 `{"data":"密文"}`(对齐 C#)。
### 5.1 请求日志 — enableRequestLog ### 5.1 请求日志 — enableRequestLog
记录每次请求的方法、URL、请求头、请求体、响应状态码、耗时与异常(对齐 C#): 记录每次请求的方法、URL、请求头、请求体、响应状态码、耗时与异常(对齐 C#):
- 请求体按 **JSON 字段级截断**(仅对超长字符串字段截断,保留结构;非 JSON 整串截断) - 请求体按 **JSON 字段级截断**(仅对超长字符串字段截断,保留结构;非 JSON 整串截断)
- 下游异常**捕获记录后重抛**(对齐 C# ExceptionDispatchInfo - 下游异常**捕获记录后重抛**(对齐 C# ExceptionDispatchInfo
- 响应体因 soulsoft `HttpResponse.body` 只读不可替换,记录 `Content-Length` 作为替代(C# 用 MemoryStream 捕获)
```cangjie ```cangjie
SimApiExtensions.addSimApi(builder) { options => SimApiExtensions.addSimApi(builder) { options =>
@@ -303,27 +314,18 @@ SimApiExtensions.addSimApi(builder) { options =>
### 5.2 日志格式 — SimApiLogger ### 5.2 日志格式 — SimApiLogger
`enableLogger`(默认 `true`)时自动使用 `SimApiLoggerProvider`(替换 soulsoft 默认控制台格式),输出格式对齐 C# 原版: `enableLogger`(默认 `true`)时自动使用 `SimApiLoggerProvider`,输出格式对齐 C# 原版:
``` ```
[ 分类 ][ 时间:毫秒 ][ 级别 ] [ 分类 ][ 时间:毫秒 ][ 级别 ]
消息内容 消息内容
``` ```
按级别着色: 按级别着色:Debug 深紫 / Info 深青 / Warn 黄 / Error 红 / Fatal 深红。
| 级别 | 颜色 |
|------|------|
| Debug | 深紫(DarkMagenta |
| Info | 深青(DarkCyan |
| Warn | 黄(Yellow |
| Error | 红(Red |
| Fatal | 深红(DarkRed 粗体近似) |
| 其他 | 白(White |
### 5.3 存储 — SimApiStorageS3/MinIO,对齐 C# SimApiStorage ### 5.3 存储 — SimApiStorageS3/MinIO,对齐 C# SimApiStorage
`enableSimApiStorage = true` 时注册 `SimApiStorage`Scoped,内部自实现 AWS Signature V4,无需 Minio SDK): `enableSimApiStorage = true` 时注册 `SimApiStorage`Scoped,内部自实现 AWS Signature V4基于 stdx.net.http无需 Minio SDK):
```cangjie ```cangjie
SimApiExtensions.addSimApi(builder) { options => SimApiExtensions.addSimApi(builder) { options =>
@@ -349,7 +351,6 @@ SimApiExtensions.addSimApi(builder) { options =>
> 说明:桶不存在时自动创建(对齐 C# BucketExists + MakeBucket,静态守卫只执行一次); > 说明:桶不存在时自动创建(对齐 C# BucketExists + MakeBucket,静态守卫只执行一次);
> 预签名与上传使用 AWS SigV4HMAC-SHA256 基于 stdx SHA256 自实现),已用 AWS 官方测试向量验证签名正确。 > 预签名与上传使用 AWS SigV4HMAC-SHA256 基于 stdx SHA256 自实现),已用 AWS 官方测试向量验证签名正确。
> 注册为 Scoped 是为了注入 `IHttpContextAccessor`soulsoft DI 禁止 singleton 消费 scoped 服务)。
### 6. 内置路由(UseSimApi 自动注册) ### 6. 内置路由(UseSimApi 自动注册)
@@ -359,7 +360,7 @@ SimApiExtensions.addSimApi(builder) { options =>
| `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 | | `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 |
| `/exception/{code}` | GET | 始终 | 错误反馈(抛 SimApiException | | `/exception/{code}` | GET | 始终 | 错误反馈(抛 SimApiException |
路由路径可自定义(`configureSimApiRoute`,自定义值通过 `mapGet/mapPost` 真实注册,默认值由内置控制器特性路由覆盖): 路由路径可自定义(`configureSimApiRoute`):
```cangjie ```cangjie
options.configureSimApiRoute { route => options.configureSimApiRoute { route =>
@@ -460,7 +461,7 @@ SimApiExtensions.addSimApi(builder) { options =>
## 内置控制器(MVC 写法) ## 内置控制器(MVC 写法)
simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),`addSimApi` 自动注册内置控制器 + 自动扫描调用者包中的控制器(对齐 C# `Assembly.GetTypes()` 扫描,见 `SimApiControllerScanner`): simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),`addSimApi` 自动注册内置控制器 + 自动扫描调用者包中的控制器(对齐 C# `Assembly.GetTypes()` 扫描):
| 控制器 | 路由 | 说明 | | 控制器 | 路由 | 说明 |
|--------|------|------| |--------|------|------|
@@ -470,7 +471,7 @@ simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),`addSimAp
```cangjie ```cangjie
import simapi.controllers.* import simapi.controllers.*
import simapi.attributes.{SimApiAuth} import simapi.annotations.{SimApiAuth}
// 控制器写法:继承 SimApiBaseController,注解路由 + DI 注入 // 控制器写法:继承 SimApiBaseController,注解路由 + DI 注入
@SimApiAuth // 类级鉴权(可选,替代 requireLogin @SimApiAuth // 类级鉴权(可选,替代 requireLogin
@@ -491,7 +492,7 @@ public class MyController <: SimApiBaseController {
## 未实现模块(选项占位) ## 未实现模块(选项占位)
以下 C# 原包功能因仓颉生态暂无对应库(Hangfire/MQTT/MinIO/Swashbuckle),**选项保留但未实现**: 以下 C# 原包功能因仓颉生态暂无对应库(Hangfire/MQTT/Swashbuckle),**选项保留但未实现**:
| 选项 | 原功能 | 状态 | | 选项 | 原功能 | 状态 |
|------|--------|------| |------|--------|------|
@@ -499,7 +500,7 @@ public class MyController <: SimApiBaseController {
| `enableSynapse` | MQTT 通信 | ❌ 未实现 | | `enableSynapse` | MQTT 通信 | ❌ 未实现 |
| `enableJob` | Hangfire 任务调度 | ❌ 未实现 | | `enableJob` | Hangfire 任务调度 | ❌ 未实现 |
> ✅ 已实现(曾为占位):`enableSimApiStorage`S3/MinIO,自实现 AWS SigV4)、`enableSimApiAuthGate`AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC,与 .NET 双向互操作)、`ISimApiAuthChecker`(注解鉴权时执行)、内置路由自定义路径。 > ✅ 已实现(曾为占位):`enableSimApiStorage`S3/MinIO,自实现 AWS SigV4)、`enableSimApiAuthGate`AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC,与 .NET 双向互操作)、`ISimApiAuthChecker`、`@SimApiSign` / `@AesBody` 声明式注解、内置路由自定义路径。
--- ---
@@ -511,9 +512,9 @@ public class MyController <: SimApiBaseController {
| `soulsoft_extensions_logging` 系列 | 日志 | | `soulsoft_extensions_logging` 系列 | 日志 |
| `soulsoft_extensions_injection` | 依赖注入 | | `soulsoft_extensions_injection` | 依赖注入 |
| `soulsoft_extensions_configuration` | 配置 | | `soulsoft_extensions_configuration` | 配置 |
| `soulsoft_serialization` | JSON 序列化 | | `simapi_serialization`path 依赖) | JSON 序列化simapi 自研,反射免标注) |
| `redis`pkg.cangjie-lang.cn | Redis 客户端(认证/缓存 Redis 模式) | | `redis`pkg.cangjie-lang.cn | Redis 客户端(认证/缓存 Redis 模式) |
| `stdx`CANGJIE_STDX_PATH | 标准扩展库(md5/sha1/base64/http | | `stdx`CANGJIE_STDX_PATH | 标准扩展库(md5/sha1/base64/http/tls |
> 构建前需设置 `CANGJIE_STDX_PATH` 指向本地 stdx 的 `static/stdx` 目录。 > 构建前需设置 `CANGJIE_STDX_PATH` 指向本地 stdx 的 `static/stdx` 目录。