SESEAI-H5三方接入集成文档

本文档面向合作方("第三方应用")的接入工程师,描述将贵方业务以完整第三方登录模式接入我方平台的流程、需要调用的接口及其请求/响应字段。

接入前,贵方应用需在我方平台完成注册,由我方分配以下凭据:app_id、api_key、redirect_url(可配置为逗号分隔的多个,便于流量分散)。secret_key 正在逐步启用,当前通过 /thirdparty/authurl 的 X-SECRET-KEY Header 传入,暂不强制要求。

接入地址(apiBaseUrl)

环境 apiBaseUrl
测试环境 https://lite-stg.zeniaiai.com
生产环境 https://h5web.seseai.com

本文档中的 H5 地址示例默认使用测试环境。生产上线时,请将 apiBaseUrl 切换为生产环境地址。


1. 集成模式选择

当前推荐使用完整第三方登录模式,贵方需要具备用户系统和支付系统:

模式 贵方用户系统 贵方支付系统 调用方式 积分管理方
完整第三方登录(is_need_pay=false) 有 有 后端 → 我方 /thirdparty/authurl + /login + /logout 全流程 贵方(资产 ↔ 积分双向同步)

核心流程:贵方负责收款和资产管理,通过 asset 字段把"等价积分"带入我方系统供消费,会话结束时我方将剩余积分以 asset 形式归还给贵方。


2. 通用约定

2.1 鉴权

第三方后端接口(/api/thirdparty/...)使用以下 Header 鉴权:

Header 说明
X-APP-ID 我方分配的应用 ID(同 body 中的 app_id 字段;Header 优先生效)
X-APP-KEY 我方分配的 API Key
X-SECRET-KEY 我方分配的Secret Key,90天有效期,到期7天前会通知更换

缺失 X-APP-KEY 直接返回 ErrInvalidInput;app_id 在 /thirdparty/authurl 上为必填。X-SECRET-KEY 缺失时保持兼容,不会因为缺失而拒绝请求。

2.2 资产 ↔ 积分换算

完整第三方登录模式的 asset 字段与我方内部积分余额按 1 : 10 换算:

points = asset × 10
asset  = points ÷ 10   (响应中的 balance 字段,保留 2 位小数)

2.3 用户标识 user_id

贵方在请求体中传入的 user_id 是贵方系统内的用户标识(最大 256 字节,去除首尾空白后非空),用于在我方系统内做一一映射。我方为同一 (app_id, user_id) 创建唯一本地账号;后续相同请求会复用该账号。

2.4 channel_code 与 is_need_pay

2.5 充值控制参数(H5 收银入口)

magic-link 跳转到我方 H5 后,是否展示「充值」入口、点击后走哪条收银路径,由以下三个参数共同决定。它们作用在不同层:

参数 所在层 来源 作用
is_need_pay 后端字段 /authurl 固定传入 false,回显到 magic-link 的 is_need_pay=false,不展示我方内置充值入口
top_up_text 后端字段 /authurl 请求体 「余额不足 / 充值」提示的 CTA 文案。典型用于 parent_support_pay=true:余额不足时 H5 弹出充值提示,按钮文案取此值,点击后经 postMessage 交由父窗口收银。按会话持久化(24h,logout 清除),原样回显在 /auth/:token 的 topUpText 字段(无默认值、不随 callback 订阅门控),不写入 magic-link URL
parent_support_pay 前端 URL 参数 贵方自行拼接到 H5 URL 非后端 API 字段,我方后端不感知。 iframe 嵌入场景下,余额不足点击充值时通过 postMessage({ type: 'topup' }) 通知父窗口,由外层系统收银;按钮文案即上面的 top_up_text

is_need_pay 固定为 false;top_up_text 与 parent_support_pay 配套使用:前者由贵方后端经 /authurl 下发文案,后者由贵方前端拼到 H5 URL 决定收银路径。H5 侧的取值规范、组合行为矩阵、postMessage 事件协议、受影响页面等完整说明见 §8。

2.6 标准响应结构

所有接口(含错误)共用以下信封:

{
  "success": true,
  "data": { ... },
  "meta": { ... }
}

错误时 success=false 并附带 error.code / error.message。


3. 完整第三方登录 — 有用户系统、有支付系统、自管积分

适用场景:贵方既有账号体系,也有自己的支付与积分体系。希望由贵方收款,再把"已购买的等价积分"以 asset 字段同步到我方,由我方代为提供 AI 能力消费;会话结束时把未消费的积分以 asset 形式归还给贵方记账。

3.1 资产生命周期

   ┌──────────────────────────────────────────────────────────────────┐
   │  贵方支付(贵方账本)                                                │
   │     ↓ 用户购买 N 资产                                                │
   │  贵方调用 /thirdparty/authurl 或 /login,asset=N                     │
   │     ↓ bring-in:我方记账 N × 10 = 10N 积分                           │
   │  用户在我方 H5 / API 消费积分                                         │
   │     ↓ 消费 M 积分(M ≤ 10N)                                         │
   │  贵方调用 /thirdparty/logout                                         │
   │     ↓ settle:我方扣减剩余 (10N − M) 积分                            │
   │     ↓ 响应 balance = (10N − M) / 10                                  │
   │  贵方据 balance 把剩余资产回写到贵方账本                                │
   └──────────────────────────────────────────────────────────────────┘

3.2 集成步骤

  1. 登录入口(推荐 /thirdparty/authurl,得到 magic-link):用户在贵方系统购买/触发后,贵方后端调用 /thirdparty/authurl:

    {
      "user_id": "{your_user_id}",
      "asset": 100.0,
      "is_need_pay": false,
      "channel_code": "{your_channel_code}",
      "nickname": "Bob",
      "lang": "zh-hans"
    }

    响应:

    {
      "success": true,
      "data": {
        "authUrl": "https://lite-stg.zeniaiai.com/cb?token=...&is_need_pay=false",
        "balance": 100.0,
        "userID": "1234567890",
        "thirdPartyUserID": "{your_user_id}"
      }
    }

    贵方前端将用户重定向到 authUrl,由我方 H5 拿 token 换 JWT 后登录。

  2. 替代入口(纯服务端,/thirdparty/login,不返回 magic-link):如果贵方完全不需要 H5、只是想在贵方页面里通过 JWT 直接调用我方 API,可改用 /thirdparty/login:

    {
      "user_id": "{your_user_id}",
      "asset": 100.0
    }

    响应是 ThirdPartyBalanceOutput(含 balance),不含 authUrl。该接口同样有 bring-in 的 session 互斥保护。

  3. 会话结束:用户结束使用、贵方主动登出时,调用 /thirdparty/logout:

    {
      "user_id": "{your_user_id}"
    }

    响应 balance 即结算时剩余的资产单位。贵方据此把剩余金额回写到贵方账本。

  4. 核对积分流水:如需对账,可拉取该用户在我方系统的消费流水:

    POST /api/thirdparty/transactions
    {
      "user_id": "{your_user_id}",
      "page": 1,
      "size": 50
    }

3.3 关键约束


4. 接口调用对比

接口 完整第三方登录模式
POST /api/thirdparty/authurl ✅
POST /api/thirdparty/login 可选(替代 authurl)
POST /api/thirdparty/logout ✅(结算)
GET /api/thirdparty/auth/:token ✅(我方 H5 内部)
POST /api/thirdparty/transactions ✅
GET /api/thirdparty/generations 可选
接收我方回调 POST(第 6 节) 可选(transaction + generate)

5. 接口参考

5.1 POST /api/thirdparty/authurl

申请一次性 magic-link,返回前端跳转 URL。

请求 Header:

Header 必需 说明
X-APP-ID ✅ 渠道 App ID;也可通过 body 的 app_id 传入,Header 优先
X-APP-KEY ✅ 渠道 API Key
X-SECRET-KEY 否 渠道 Secret Key,最大 100 个字符;当前处于兼容期

Secret Key 鉴权与保存规则:

  1. 如果 X-APP-KEY 在我方 third_party_apps 中不存在,会调用 Titan v2 鉴权接口:

    GET {TITAN_API}/aiapi/api/v2/mch/auth
    X-API-KEY: {X-APP-KEY}
    X-SECRET-KEY: {X-SECRET-KEY}  # 仅在前端传入非空值时发送

    Titan 鉴权成功后创建 third_party_apps 记录,并保存 api_key 与 secret_key。未传 Secret Key 时,secret_key 保存为空值。

  2. 如果 X-APP-KEY 已存在:

    历史应用的 secret_key 为空时,第一次传入非空 Secret Key 会触发鉴权,成功后写入。

  3. X-SECRET-KEY 不会出现在响应中。后续版本可能会将该 Header 改为必填。

请求 Body:

字段 类型 必需 说明
user_id string ✅ 贵方用户标识,≤ 256 字节,去空白后非空
asset float64 否 等价资产数量;>= 0 时触发 bring-in
is_need_pay bool ✅ 必须传入 false,表示由贵方负责收款和资产管理;不接受 true
channel_code string 否 渠道编码,省略时回退到 app_id
nickname string 否 用户昵称(仅写一次:首次创建;后续不会覆盖资料)
avatar string 否 用户头像 URL
lang string 否 语言代码,默认 zh-hans,会回显到 magic-link URL
theme string 否 主题(如 dark、light),回显到 magic-link URL
top_up_text string 否 「余额不足 / 充值」CTA 文案,典型配合 parent_support_pay=true(见 §2.5);按会话持久化(24h,logout 清除),原样回显在 /auth/:token 的 topUpText 字段(无默认值、独立于 callback),不写入 magic-link URL
app_id string 否 与 X-APP-ID 二选一;Header 优先
callback object 否 订阅本会话的事件回调,详见 第 6 节;字段:url (https, 必填), events (["transaction", "generate"] 子集,省略=全部), share_text (用于前端 share UI 文案,省略时回落到默认值 分享你的AI创作)

响应 data:

字段 类型 说明
authUrl string 一次性 magic-link,含 token、theme、lang、is_need_pay 查询参数
balance float64 用户当前在我方系统的余额(资产单位,积分 ÷ 10)
userID string 我方分配的用户 ID(Snowflake,字符串)
thirdPartyUserID string 回显贵方传入的 user_id

示例:

curl -X POST 'https://api.example.com/api/thirdparty/authurl' \
  -H 'X-APP-ID: partner-app-1' \
  -H 'X-APP-KEY: ak_xxxxxx' \
  -H 'X-SECRET-KEY: sk_xxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{
    "user_id":"u-42",
    "asset":100.0,
    "is_need_pay":false,
    "channel_code":"chan-a",
    "lang":"zh-hans"
  }'

5.2 POST /api/thirdparty/login

纯服务端登录,不返回 magic-link——只创建/复用用户、做 bring-in、返回当前余额。可作为 authurl 的替代。

请求 Header:X-APP-ID, X-APP-KEY

请求 Body:

字段 类型 必需 说明
user_id string ✅ 贵方用户标识
asset float64 否 同 authurl;>= 0 触发 bring-in,< 0 跳过
app_id string 否 与 X-APP-ID 二选一;Header 优先

响应 data (ThirdPartyBalanceOutput):

字段 类型 说明
userID string 我方用户 ID
thirdPartyUserID string 回显 user_id
balance float64? 当前余额(资产单位)

5.3 POST /api/thirdparty/logout

结束会话并触发余额结算(drain)。

请求 Body:

字段 类型 必需 说明
user_id string ✅ 贵方用户标识
app_id string 否 与 X-APP-ID 二选一

响应 data:

字段 类型 说明
userID string 我方用户 ID
thirdPartyUserID string 回显 user_id
balance float64? 首次 logout(本次 bring-in 周期内还没有结算过)时返回,单位为资产单位(积分 ÷ 10),反映 logout 那一刻被划走的剩余资产;重复 logout 时此字段省略
old_balance float64? 重复 logout 时返回(本次 bring-in 周期内已经有过 logout 结算行),值等于上一次真正划走的资产单位;首次 logout 时此字段省略

首次 vs 重复 logout 的判定:

幂等性:若用户从未登录过(identity 不存在),接口返回 ErrAuthInvalid;若 session 归属其他 app_id,返回 ErrResourceConflict;其他情况下重复 logout 不会报错——重复语义见上文 balance / old_balance 字段说明。

5.4 GET /api/thirdparty/auth/:token

通常由我方 H5 内部调用,贵方一般不直接调用——magic-link 中的 token 被前端解析后向此端点换取 JWT。仅作参考说明。

用一次性 token 换取 LoginOutput(含 JWT accessToken / refreshToken)。 查询参数 lang 可选,若提供且与用户已存语言不同则更新。 token 在 24 小时内多次调用都有效,并在下次 authurl 触发轮换。

响应在 LoginOutput 字段基础上额外返回三个 thirdparty 专属字段。其中 share / shareText 由本会话 callback 配置派生(详见 第 6 节);topUpText 独立于 callback,由 /authurl 的 top_up_text 直接下发(见 §2.5):

字段 类型 说明
share bool 当且仅当 callback.events 订阅了 generate 时为 true;前端据此决定是否渲染 share/分享开关
shareText string share=true 时回显贵方传入的 callback.share_text(或默认值),用于前端 share UI 文案;share=false 时省略
topUpText string 原样回显贵方在 /authurl 传入的 top_up_text(无默认值、不依赖 callback、不依赖 share);未传则省略。供前端自定义「余额不足 / 充值」按钮文案

5.5 POST /api/thirdparty/transactions

拉取我方系统内某用户的"消费"流水。返回结果按调用方 app_id 限定,且只包含 type=CONSUME 的消费记录——bring-in、退款(REFUND)、奖励等其它类型不在此列。

请求 Header:X-APP-ID, X-APP-KEY

请求 Body:

字段 类型 必需 说明
user_id string 否 不传时返回该 app_id 下全部用户
app_id string 否 与 X-APP-ID 二选一;Header 优先
page int 否 偏移分页页码,默认 1(仅在不传 cursor 时生效)
size int 否 每页大小,默认 10,最大 10000
cursor int64 否 游标分页(向后/更旧):上一响应 meta.pagination.next_cursor 的值
prev_cursor int64 否 游标分页(向前/更新):上一响应 meta.pagination.prev_cursor 的值
sortBy / sortOrder string 否 偏移模式下的排序字段与方向;游标模式下被忽略。省略时默认按交易 ID(≈ createdAt)倒序

两种分页模式(始终返回 §2.6 信封;data 为流水数组,分页信息在 meta.pagination):

meta.pagination 字段:

字段 说明
mode "offset" 或 "cursor",标识本次响应的分页模式
total 当前过滤条件下的总条数(已按 app_id/provider 限定)
pages ceil(total/size),供展示
size / page 生效的每页大小 / 页码
has_next 是否还有下一页(更旧方向);游标模式由 next_cursor 是否存在决定
has_prev 是否有上一页(更新方向);游标模式由 prev_cursor 是否存在决定
cursor 本次请求使用的向后游标(向后翻页时回显;首页/偏移/向前翻页省略)
next_cursor 下一页(更旧)游标;无更多数据时省略
prev_cursor 上一页(更新)游标;无更新数据时省略

响应 data(交易记录数组,按交易 ID 倒序;每条记录字段):

字段 类型 说明
id string 交易记录 ID(Snowflake,字符串)。游标分页的 cursor / next_cursor / prev_cursor 即取此值
userID string 我方内部用户 ID(Snowflake,字符串)
thirdPartyUserID string? 回显贵方用户标识;不传 user_id 拉取全部用户时用于区分记录归属
type string 交易类型,本接口恒为 CONSUME
amount float64 带符号的积分变动量,消费为负值
balanceAfter float64 交易后用户在我方系统的积分余额
source string 积分变动来源标识;AI 生成消费为 ai_task_consume
generationID string? 触发本次消费的 AI 生成记录 ID(Snowflake);同一值即 generate 回调的 event_id,可据此与 /thirdparty/generations 关联
bid string? 外部任务 ID(来自 AI 供应商,便于贵方溯源)
referenceID string? 关联引用 ID(若有)
description string? 交易描述(若有)
metadata object? 附加元数据(若有)
status string 交易状态(pending / completed / failed,消费记录通常为 completed;内部字段,贵方一般无需关心)
retryCount int 重试计数(内部字段)
createdAt string 交易创建时间(RFC3339)
updatedAt string? 交易更新时间(RFC3339,若有)

单位提醒:本接口 data[].amount / balanceAfter 是我方内部积分原始值,与 §6.4 回调 payload 中已换算为资产单位的 costs / balance 不同。贵方按 1 asset = 10 points 换算(积分 ÷ 10)即可与自身账本对账。

响应示例(偏移模式首页):

{
  "success": true,
  "data": [
    {
      "id": "1739450000000000001",
      "userID": "1234567890",
      "thirdPartyUserID": "u-42",
      "type": "CONSUME",
      "amount": -100,
      "balanceAfter": 900,
      "source": "ai_task_consume",
      "generationID": "1739450000000000000",
      "status": "completed",
      "createdAt": "2026-05-26T08:30:00Z"
    }
  ],
  "meta": {
    "request_id": "req-abc123",
    "timestamp": "2026-05-26T08:31:00Z",
    "pagination": {
      "mode": "offset",
      "total": 128,
      "pages": 13,
      "size": 10,
      "page": 1,
      "has_next": true,
      "has_prev": false,
      "next_cursor": "1739450000000000001"
    }
  }
}

/api/thirdparty/transactions/all 是上述接口忽略 user_id 的便捷形式,已标记 Deprecated,新接入请直接使用 /transactions 并省略 user_id。 其分页行为与 /transactions 完全一致。

5.6 GET /api/thirdparty/generations

列出该 app_id 下所有用户(或指定单用户)的 AI 生成记录,用于核对生成产物、消耗与审核状态。它是 generate 回调(第 6 节)的拉取式补充:回调由我方主动推送单条产物,本接口供贵方按需分页回查。

注意:本接口是 GET,参数走 query string(其余 thirdparty 接口多为 POST + JSON body)。

请求 Header:X-APP-ID, X-APP-KEY

请求 Query 参数:

参数 类型 必需 说明
app_id string 否 与 X-APP-ID 二选一;Header 优先
user_id string 否 贵方用户标识;省略时返回该 app_id 下全部用户的生成记录
status string 否 生成状态过滤,可重复传参或逗号分隔多值(如 status=completed,failed)
tool_id string 否 按 AI 工具 ID 过滤
audit_status string 否 审核状态过滤:pending / passed / rejected
created_at_from string 否 创建时间下界(含),RFC3339
created_at_to string 否 创建时间上界(含),RFC3339
page int 否 偏移分页页码,默认 1(仅在不传 cursor/prev_cursor 时生效)
size int 否 每页大小,默认 10,最大 10000
cursor int64 否 游标分页(向后/更旧):上一响应 meta.pagination.next_cursor 的值;传入即切换到游标模式
prev_cursor int64 否 游标分页(向前/更新):上一响应 meta.pagination.prev_cursor 的值

sortBy / sortOrder / keyword / deleted 不支持;两种分页模式都已排除软删除记录。分页有两种模式(与 §5.5 /transactions 一致): - 偏移模式(默认,不传游标):用 page / size 翻页,结果按 createdAt 倒序。整页时响应也会带 next_cursor,据此即可切入游标遍历。 - 游标模式(传 cursor 或 prev_cursor,优先于 page):按生成记录 id 倒序稳定翻页,不受深翻页性能衰减影响,大数据集请优先使用。向后/向前翻页与游标回传规则同 §5.5。

响应 data(生成记录数组,按创建时间倒序;每条记录字段):

字段 类型 说明
id string 生成记录 ID(Snowflake,字符串);即 generate 回调的 event_id,也对应 §5.5 消费流水的 generationID
thirdPartyUserID string 回显贵方用户标识
status string? 生成状态(如 pending / completed / failed)
auditStatus string? 审核状态:pending / passed / rejected
toolID string? 生成所用 AI 工具 ID(Snowflake)
aiTool object? 该工具的简要信息;无工具或工具未知时省略。子字段:id(Snowflake 字符串)、name(工具标识名)、displayName(多语言显示名对象,如 {"en":"Text to Image"})
provider string? 该工具背后的 AI 生成供应商(如 titan_aiapi),非 oauth 租户 provider
costInPoints float64 本次生成消耗的积分(÷ 10 得资产单位)
resultUrls string[]? 生成产物的结果 URL 数组;生成完成前或无产物时省略
createdAt string 创建时间(RFC3339)

安全与差异说明:出于安全考虑,本接口刻意不返回加密密钥、提示词 / 输入参数(prompt、generation_parameters)及内部 metadata,避免向接入方泄露用户内容或媒体密钥。resultUrls 是数据库存储的原始值,不保证像 §6.4 回调 assets_url 那样用应用 BaseURL 补全为绝对 URL;若为根相对路径需贵方自行拼接域名。

meta.pagination 字段:与 §5.5 完全一致。偏移模式 mode="offset",含 total / pages / size / page / has_next / has_prev,整页时附 next_cursor;游标模式 mode="cursor",has_next / has_prev 由 next_cursor / prev_cursor 是否存在决定。

示例:

curl -G 'https://api.example.com/api/thirdparty/generations' \
  -H 'X-APP-ID: partner-app-1' \
  -H 'X-APP-KEY: ak_xxxxxx' \
  --data-urlencode 'user_id=u-42' \
  --data-urlencode 'status=completed' \
  --data-urlencode 'audit_status=passed' \
  --data-urlencode 'page=1' \
  --data-urlencode 'size=20'

响应:

{
  "success": true,
  "data": [
    {
      "id": "1739450000000000000",
      "thirdPartyUserID": "u-42",
      "status": "completed",
      "auditStatus": "passed",
      "toolID": "880123456789",
      "aiTool": {
        "id": "880123456789",
        "name": "text-to-image",
        "displayName": { "en": "Text to Image", "zh-hans": "文生图" }
      },
      "provider": "titan_aiapi",
      "costInPoints": 100,
      "resultUrls": [
        "https://cdn.example.com/.../foo.png"
      ],
      "createdAt": "2026-05-26T08:30:00Z"
    }
  ],
  "meta": {
    "request_id": "req-def456",
    "timestamp": "2026-05-26T08:31:00Z",
    "pagination": {
      "total": 42,
      "pages": 3,
      "size": 20,
      "page": 1,
      "has_next": true,
      "has_prev": false
    }
  }
}

6. 回调订阅 (Callback / Webhook)

适用于完整第三方登录模式。贵方在 /thirdparty/authurl 请求体中携带 callback 对象,订阅本会话内由我方主动推送的事件。订阅是按会话生效的——配置随 session 一同写入 Redis,logout 时一并清除。

6.1 订阅方式

在 POST /api/thirdparty/authurl 的请求体中追加 callback 对象:

{
  "user_id": "{your_user_id}",
  "asset": 100.0,
  "callback": {
    "url": "https://partner.example.com/seselite/cb",
    "events": ["transaction", "generate"],
    "share_text": "分享你的AI创作"
  }
}
字段 类型 必需 说明
url string ✅ 我方将事件 POST 到此 URL,必须 https,不跟随 3xx 跳转
events string[] 否 订阅的事件名,子集 ⊆ ["transaction", "generate"];省略或空数组等价于全部订阅;大小写不敏感,去重
share_text string 否 前端 share/分享 UI 的文案;省略时默认 分享你的AI创作(仅在 events 含 generate 时通过 /auth/:token 回显给前端)

重复调用 /authurl 的语义:

6.2 事件目录

action 触发时机 适用模式 说明
consume 用户消费积分(AI 生成完成、AI 工具调用等) 当前模式 由 events: ["transaction", ...] 订阅
refund AI 生成失败、退款回写积分 当前模式 同上
generate AI 生成完成,资产 URL 已就绪 当前模式 由 events: [..., "generate"] 订阅,且 提交时 share=true(见 6.3)

6.3 generate 事件的 per-submit opt-in

generate 事件除了需要会话级 events 订阅,还需要单次 AI 提交时显式 opt-in:

6.4 POST 请求格式

我方向 callback.url 发起的请求:

POST /seselite/cb HTTP/1.1
Host: partner.example.com
Authorization: Bearer {your_api_key}
Content-Type: application/json
X-Seselite-Event-Id: {action}:{event_id}

{
  "action": "consume",
  "user_id": "{your_user_id}",
  "event_id": "1234567890",
  "data": { ... }
}
字段 / Header 说明
Authorization: Bearer ... 值即我方分配给贵方的 api_key(与贵方调用我方接口时用的 X-APP-KEY 同值);贵方应据此校验请求来自我方
X-Seselite-Event-Id {action}:{event_id} 的冗余拷贝,便于在网关/代理层做 header 级去重
action consume / refund / generate
user_id 贵方系统内的 user_id(同贵方 /authurl 请求中传入的值)
event_id 字符串化的事件 ID:consume / refund 为我方 point_transactions.id,generate 为 ai_generations.id
data 事件载荷,按 action 不同;见下文

data 字段形状:

// action=consume:amount 为负数(积分),costs 为正数(资产单位),balance 为交易后余额(资产单位)
{
  "action": "consume", "user_id": "u-42", "event_id": "456",
  "data": { "amount": -100, "costs": 10, "balance": 90 }
}

// action=refund:amount 为正数(积分),costs 为正数(资产单位,等于 |amount|/10),balance 为交易后余额(资产单位)
{
  "action": "refund", "user_id": "u-42", "event_id": "789",
  "data": { "amount": 100, "costs": 10, "balance": 100 }
}

// action=generate:assets_url 为该次生成产出的资产**完整 URL**数组
{
  "action": "generate", "user_id": "u-42", "event_id": "abc",
  "data": {
    "assets_url": [
      "https://api.example.com/files/frcreative/users/.../foo.png",
      "https://cdn.example.com/.../bar.mp4"
    ]
  }
}

consume / refund 的 data 字段(详):

字段 类型 单位 说明
amount float64 积分 带符号:consume 为负、refund 为正。保留为向后兼容字段,单位与 costs / balance 不同。
costs float64 资产 ABS(amount) / 10(保留 2 位小数),即本次交易消耗/返还的资产单位绝对值,可直接和贵方账本比对。
balance float64 资产 本次交易之后用户在我方系统的剩余余额,单位为资产;等于 point_transactions.balance_after / 10。

三个字段都按 1 asset = 10 points 换算;提供 costs 与 balance 是为了让贵方无需在本地再做点对资产的换算,减少四舍五入歧义。

data.assets_url 中的每个元素都是绝对 URL:我方在入箱时若资产路径为 /… 这样的根相对路径,会用应用基础 URL(App.BaseURL)补齐;已经是 http(s):// 开头的则原样透传。贵方可直接对 URL 发起 GET 下载,无需拼接前缀。

6.5 投递语义与重试

6.6 端点最低要求

贵方实现的 callback 端点需满足:

  1. HTTPS——HTTP URL 在 /authurl 校验阶段即被拒。
  2. 校验 Authorization Bearer 头——值必须与我方分配的 api_key 一致;不一致返回 401(仍会被重试,但便于排查)。
  3. 2xx ⇔ 已持久化——返回 2xx 之前确保事件已落地(数据库 / 队列),避免丢事件。
  4. 按 (action, event_id) 幂等——同一 event_id 重复到达视为同一事件,不重复结算。
  5. 限时响应——10 秒内返回;耗时操作请异步化。

6.7 会话结束 — 配置清理

POST /thirdparty/logout 在事务提交后会异步删除 Redis 中的 thirdparty:session_cfg:<userID> 键:

6.8 联调用:/api/debug/echo

我方在开发环境(DevMode=true)下提供一个回显端点用于贵方排查 callback:

POST  /api/debug/echo
GET   /api/debug/echo

7. 错误处理

通用错误码(详见 docs/ERROR_CODE_SYSTEM.md):

HTTP 业务码语义 触发场景
400 ErrInvalidInput 缺 app_key、user_id 为空或超长、is_need_pay 类型不对等参数错误
401 ErrAuthInvalid api_key 错误;/logout 时 user_id 在该 app_id 下不存在
409 ErrResourceConflict /logout 时发现 session 归属于另一个 app_id(用户已被别的应用接管)
503 ErrCacheUnavailable Redis 不可用,会话读写失败

logout 在写入余额时若发生数据库异常会保留 Redis session,方便贵方按相同 user_id 重试。


8. H5 充值入口控制参数详解

本节面向以 Webview / iframe 形式嵌入或深度集成我方 H5 的贵方前端工程师,详述 §2.5 概述的三个充值控制参数在 H5 内的完整行为。is_need_pay 固定为 false,由后端经 magic-link 回显;top_up_text 由后端经 /authurl 下发;parent_support_pay 则由贵方前端自行拼接到 H5 URL。

8.1 取值规范

is_need_pay 仅接受 false。parent_support_pay 仍支持 yes / no / true / false,系统会自动归一化为 yes 或 no。

8.2 is_need_pay — 充值开关

在 magic-link 流程中,该值由后端 /authurl 的 is_need_pay=false 回显进 URL(见 §2.4)。

8.3 parent_support_pay — 父级代充值开关

适用场景:平台被嵌入到第三方站点或聚合平台;外层系统已有成熟的支付体系,希望统一收银台。

事件协议:当 parent_support_pay=yes 且检测到处于 iframe 环境时,点击充值按钮会触发:

window.parent.postMessage({ type: 'topup' }, '*');

父级页面监听示例:

window.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'topup') {
    // 打开外层系统的充值弹窗或跳转至收银台
    openYourOwnPaymentModal();
  }
});

parent_support_pay 不是后端 API 字段,我方后端不感知它;贵方需自行将其拼接到 H5 URL。

8.4 top_up_text — 自定义充值按钮文案

充值按钮(含余额不足提示框的 CTA)的文案可由后端经 /authurl 的 top_up_text 字段下发(见 §5.1),前端从 /auth/:token 响应的 topUpText 读取:

8.5 组合行为矩阵

is_need_pay parent_support_pay 环境 充值按钮 点击行为
false no 任意 ❌ 隐藏 —
false yes iframe 内 ✅ 显示 向父窗口 postMessage
false yes 独立页 ❌ 隐藏 —(无父窗口可通知)

is_need_pay 固定为 false,不启用我方内置充值;是否显示充值入口仅由 parent_support_pay 和页面是否处于 iframe 决定。

8.6 受影响的功能页面

以下页面会根据上述参数动态调整充值入口:

8.7 接入示例

① iframe 嵌入,由外层统一收银

<iframe
  src="https://lite-stg.zeniaiai.com/?is_need_pay=false&parent_support_pay=yes"
  width="100%"
  height="100%"
></iframe>

<script>
  window.addEventListener('message', (e) => {
    if (e.data?.type === 'topup') {
      // 调起外层支付组件
      yourPaymentSDK.open({ userId: 'xxx' });
    }
  });
</script>

② 完全关闭充值(纯展示 / 内测场景)

https://lite-stg.zeniaiai.com/?is_need_pay=false&parent_support_pay=no

8.8 常见问题

Q:参数修改后没有生效? A:参数仅在应用首次加载(onLaunch)时从 URL 解析一次。若需要变更,请刷新页面或重新打开 Webview。

Q:is_need_pay 可以传其他值吗? A:不可以。当前仅接受 is_need_pay=false;需要充值时,请在 iframe 场景下使用 parent_support_pay=yes,由贵方收银系统处理。

Q:如何在原生 App(Capacitor)中控制充值? A:当前参数主要针对 Web / H5 / iframe 场景。原生 App 的充值开关通常由构建配置或后端接口控制,不在本文档范围内。


9. Android WebView 接入 H5 与打开外部浏览器

本节说明 Android 原生 App 在 WebView 中加载本 H5,下载文件被系统拦截的解决方案

9.1 WebView 基本配置

H5 登录需要 JavaScript 和 DOM Storage。WebView 容器及其父布局应提供有界的实际高度(通常为 match_parent),否则 H5 的 100vh / 100dvh 布局可能得到零高度,表现为黑屏或空白。先使用 Android 系统 WebView;AndroidX WebKit 是 WebView 的扩展兼容库,不能替代或升级设备上的 Chromium 内核。

webView.settings.apply {
    javaScriptEnabled = true
    domStorageEnabled = true
}

// 在 loadUrl 前注册;加载 authUrl 与直接加载 H5 均适用。
webView.addJavascriptInterface(
    ExternalBrowserBridge(webView, this),
    "AndroidBridge",
)
webView.loadUrl(authUrl) // 或 https://lite-stg.zeniaiai.com/ 等 H5 地址

以上示例假设代码位于 Activity,this 是 Context。不要向不可信网页或混合了第三方内容的 WebView 暴露此接口;addJavascriptInterface 对页面 frame 可见,下面检查的是顶层页面 URL,并不能认证调用来自哪个 iframe。

9.2 添加原生 bridge

在 Android 项目中新建 Kotlin 类(包名按项目调整)。示例只允许从测试/生产 H5 域名调用,并只接受目标 HTTP(S) URL;如果 H5 实际页面还会使用其他受控子域,请将经过确认的完整域名加入白名单。

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.Toast
import java.util.Locale

class ExternalBrowserBridge(
    private val webView: WebView,
    context: Context,
) {
    private val appContext = context.applicationContext
    private val mainHandler = Handler(Looper.getMainLooper())
    private val trustedHosts = setOf(
        "lite-stg.zeniaiai.com",
        "h5web.seseai.com",
    )

    @JavascriptInterface
    fun openExternalBrowser(rawUrl: String?) {
        // JavaScript bridge 回调不保证运行在 UI 线程。
        mainHandler.post {
            val page = webView.url?.let(Uri::parse) ?: return@post
            val pageHost = page.host?.lowercase(Locale.ROOT)
            if (page.scheme != "https" || page.port !in setOf(-1, 443) ||
                page.userInfo != null || pageHost !in trustedHosts
            ) return@post

            val target = rawUrl?.trim()?.takeIf { it.isNotEmpty() }
                ?.let(Uri::parse) ?: return@post
            if (target.scheme?.lowercase(Locale.ROOT) !in setOf("http", "https") ||
                target.host.isNullOrBlank() || target.userInfo != null
            ) return@post

            val intent = Intent(Intent.ACTION_VIEW, target).apply {
                addCategory(Intent.CATEGORY_BROWSABLE)
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
            try {
                appContext.startActivity(intent)
            } catch (_: ActivityNotFoundException) {
                Toast.makeText(appContext, "没有可打开此链接的应用", Toast.LENGTH_SHORT).show()
            } catch (_: SecurityException) {
                Toast.makeText(appContext, "无法打开此链接", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

Activity 退出或销毁 WebView 时移除接口并销毁 WebView,避免持有过期对象:

webView.removeJavascriptInterface("AndroidBridge")
webView.stopLoading()
webView.destroy()

如启用 R8/ProGuard,请保留 JavaScript 接口注解方法:

-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

9.3 H5 调用和行为

该工具会尝试调用 window.AndroidBridge.openExternalBrowser(url);因此原生对象名和方法名须保持一致。Android 的 @JavascriptInterface 方法返回时并不表示外部 App 已成功打开,因为原生启动在主线程异步执行。若系统没有可处理目标地址的应用或系统拒绝启动,会显示原生提示。

ACTION_VIEW 与 CATEGORY_BROWSABLE 将链接交给 Android 的外部 URL 处理器。普通 HTTP(S) 链接通常由默认浏览器打开;若用户没有设置默认项,系统可能显示应用选择器;若其他 App 注册了匹配的 App Links,也可能由该 App 打开。该机制请求系统处理链接,不保证一定启动某个指定浏览器。

若未注册桥接或调用失败,H5 工具可能退回 window.open;WebView 内这通常只会打开 WebView 新窗口(还需宿主实现 WebChromeClient.onCreateWindow),不会自动启动手机默认浏览器。请在真机上从实际 H5 页面点击测试,并确认最终页面域名包含在 trustedHosts 中。