Skip to content

Agreement 用户协议(前端调用视角) ​

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

一、域特征(读端点前先看这四条) ​

#特征对前端意味着什么
1两个端点全免登录auth:'none' —— 协议在认证之前就要能看(登录页底部链接)。别在这里做 token 前置判断
2恒 HTTP 200,无业务错误码唯一失败形态是 data: null(非法 type / 无记录)—— 没有 catch 可抓、没有码值可分支,只能判空
3type 白名单只有两个值user_agreement(用户协议)/ privacy(隐私政策)。传白名单外的值不报错,返回 data: null → 协议页静默空白(最坏形态:用户以为协议是空的)
4🔴 两个响应字段组是全项目唯一的 snake_caseupdated_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。

失败时的前端动作

情形前端动作
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[].typeuser_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对象 或 nullHTTP 200 + data:null(非法 type / 无记录)
GET /api/v1/content/agreementslistAgreements()无(零调用)数组同上,data 为 []

字段表 / 类型 / 可空 / curl → 手册 · agreement。 本页与手册的分工边界 → api/ 总览。