---
url: /docs/guide/uniapp/api/agreement.md
---
# Agreement 用户协议（前端调用视角）

> **契约权威** → [手册 · agreement](/api-docs/v1/frontend/agreement)：字段表 / 类型 / 可空 / curl **一律在那**，本页零复述。
> **本页回答**：哪个页面在什么时机调、响应从哪一层读、失败了前端做什么。
> 封装 `src/api/agreement.ts`（api 层 6 文件之一，**最小的一份：2 个端点**）· 消费页 [pages/agreement](../pages/agreement.md) + 登录页两协议链接

## 一、域特征（读端点前先看这四条）

| # | 特征 | 对前端意味着什么 |
|---|---|---|
| 1 | **两个端点全免登录** | `auth:'none'` —— 协议在**认证之前**就要能看（登录页底部链接）。别在这里做 token 前置判断 |
| 2 | **恒 HTTP 200，无业务错误码** | 唯一失败形态是 `data: null`（非法 `type` / 无记录）—— **没有 `catch` 可抓、没有码值可分支**，只能判空 |
| 3 | **`type` 白名单只有两个值** | `user_agreement`（用户协议）/ `privacy`（隐私政策）。传白名单外的值**不报错**，返回 `data: null` → 协议页**静默空白**（最坏形态：用户以为协议是空的） |
| 4 | 🔴 **两个响应字段组是全项目唯一的 snake\_case** | `updated_at` —— 其余 28 个端点一律 camelCase。**勿"顺手改成 camelCase"**，前端类型声明已对齐 |
| 5 | 🆕 **`content_format` 决定渲染方式** | `plain`=纯文本按段（存量）；`html`=服务端已 sanitize 的富文本 → 用 **`rich-text` 组件**渲染（**勿 `v-html`**）。旧版 App 不认该字段 → 行为不变（向后兼容） |

### ⚠️ 失败是静默的（本页的头号坑）

后端对非法 `type` 走 `cz__ok(None)`：**HTTP 200 + code `"0"` + `data: null`**，不 500、不报错。
⇒ 前端**必须显式判空**，不能把"请求成功"当成"内容拿到了"。

```ts
// 正确：code 与 data 一起判（协议页现有写法，保持）
if (res?.code === '0' && res.data) content.value = res.data;
// ❌ 反例：直接 res.data.content → 非法 type 时抛 TypeError，页面白屏
```

***

## 二、端点明细

### getAgreement(type)（`src/api/agreement.ts` → `getAgreement('user_agreement' | 'privacy')`）

**一句话**：协议详情页 `onLoad` 按 `type` 拉单条正文 —— 后台可更新内容，**App 不必发版**。

**前端函数签名**

| 项 | 值 |
|---|---|
| 封装 | `src/api/agreement.ts` → `getAgreement(type)` |
| 路径 | `GET api/v1/content/agreements/{type}` |
| 触发时机 | `pages/agreement/index.vue` 的 `onLoad`（从登录页链接或「我的」页跳入） |
| 鉴权 | **`none`（公开）** |

**必需 payload 键**

| 键 | 从哪来 | 必填 |
|---|---|---|
| `{type}` (path) | 页面 `onLoad` 的 `query.type`（**归一后再传**，见坑位） | ✔ |

**响应读法**

```ts
getAgreement(type).then((res: any) => {
	if (res?.code === '0' && res.data) content.value = res.data;   // 中文注解：判空是必须的，见上「静默失败」
});
// 🆕 富文本（2026-09-24）：按 content_format 分支渲染
const isHtml = computed(() => content.value?.content_format === 'html');
// html → rich-text 内置组件（白名单渲染、不执行 script）；勿 v-html
const richNodes = computed(() => content.value?.content || '');
// plain（存量）→ 照旧按空行切段
const paragraphs = computed(() =>
	isHtml.value ? [] : (content.value?.content || '').split(/\n{2,}/).filter(p => p.trim()),   // 中文注解：plain 才是纯文本
);
```

**响应真值**

| 路径 | 含义 |
|---|---|
| `data`（整体） | **可能是 `null`** —— 非法 type / 无记录 |
| `data.title` | 协议标题（页面 `title` 优先用跳转参数，拿不到才用它） |
| `data.content` | **纯文本全文**，空行分段 —— ⚠️ **不是 HTML/富文本**，别直接塞 `rich-text` |
| `data.version` | 版本号（后台每次更新自增） |
| `data.updated_at` | 更新时间（**snake\_case**） |

> 字段类型/可空性见 [手册 · getAgreement](/api-docs/v1/frontend/agreement#getagreement)。

**失败时的前端动作**

| 情形 | 前端动作 |
|---|---|
| `data === null`（**HTTP 200**） | **不要报错弹窗** —— 页面显示「协议内容暂不可用」兜底文案；这是"内容缺失"不是"网络故障" |
| 网络/5xx | 同上兜底 + 日志；协议页**不阻断用户返回**（登录页链接必须能退回） |

**⚠️ 坑位**

* **`type` 由标题文案反推**：登录页 `showAgreement(title)` 用 `title.includes('隐私') ? 'privacy' : 'user_agreement'` 生成 ——
  ⇒ **改登录页协议链接文案时别动「隐私」二字**，否则隐私政策会静默变成用户协议（页面不报错，内容却是错的）。
* **协议页 `onLoad` 自己又归一了一次**（`query.type === 'privacy' ? 'privacy' : 'user_agreement'`）——
  这是有意为之的兜底：外部跳转传了非法 `type` 也不会打到 `data: null` 分支。
  **新调用点请沿用同一归一写法**，别把 `query.type` 直接透传。

***

### listAgreements()（`src/api/agreement.ts` → `listAgreements()`）

**一句话**：一次拉全部协议（用户协议 + 隐私政策）。**当前前端无任何调用点** —— 保留给"启动时预热协议内容"之类的场景。

**前端函数签名**

| 项 | 值 |
|---|---|
| 封装 | `src/api/agreement.ts` → `listAgreements()`（**无入参**） |
| 路径 | `GET api/v1/content/agreements` |
| 触发时机 | **无**（2026-09-20 实查：全仓零调用；登录页是两个 `navigateTo` 链接，不是一次拉两类） |
| 鉴权 | **`none`（公开）** |

**必需 payload 键**：无。

**响应读法**（顶层直接是数组）

```ts
const res = await listAgreements();
const items = res?.data ?? [];                                  // 中文注解：data 就是数组
const privacy = items.find(i => i.type === 'privacy');          // 中文注解：按 type 取，不要按 index 取
```

**响应真值**

| 路径 | 含义 |
|---|---|
| `data[].type` | `user_agreement` / `privacy`（**唯一可用的取用键**） |
| `data[].title` / `.content` | 同单条端点 |
| `data[].version` / `.updated_at` | 同单条端点（snake\_case） |

**失败时的前端动作**

| 情形 | 前端动作 |
|---|---|
| 任意失败 | 保空数组（当前无调用点，新接入方自行兜底） |
| **空数组** | 正常态 —— 与单条端点的 `data: null` **含义不同**，别混用判空逻辑 |

***

## 三、一页速查

| 端点 | 前端函数 | 调用点 | `data` 形态 | 失败形态 |
|---|---|---|---|---|
| `GET /api/v1/content/agreements/{type}` | `getAgreement(type)` | `pages/agreement/index.vue` | 对象 **或 `null`** | **HTTP 200 + `data:null`**（非法 type / 无记录） |
| `GET /api/v1/content/agreements` | `listAgreements()` | **无（零调用）** | 数组 | 同上，`data` 为 `[]` |

> 字段表 / 类型 / 可空 / curl → [手册 · agreement](/api-docs/v1/frontend/agreement)。
> 本页与手册的分工边界 → [api/ 总览](./index.md)。
