803 lines
30 KiB
Markdown
803 lines
30 KiB
Markdown
|
|
# AI 绘本创作系统 — 企业定制对接指南 V3.1
|
|||
|
|
|
|||
|
|
> 版本: V3.1 | 更新日期: 2026-04-03
|
|||
|
|
> 适用客户: 自有 C端 H5 + 管理后台,需嵌入乐读派 AI 创作能力
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、整体架构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 客户自有 C端 H5 │
|
|||
|
|
│ ┌──────┐ ┌──────────────┐ ┌──────┐ ┌──────┐ │
|
|||
|
|
│ │ 广场 │ │ 创作(iframe)│ │ 作品 │ │ 我的 │ │
|
|||
|
|
│ │ │ │ ┌──────────┐ │ │ │ │ │ │
|
|||
|
|
│ │ 优秀 │ │ │乐读派H5 │ │ │ AI作品│ │ 个人 │ │
|
|||
|
|
│ │ 作品 │ │ │上传→提取 │ │ │ + │ │ 设置 │ │
|
|||
|
|
│ │ 展示 │ │ │→画风→创作│ │ │ 自有 │ │ │ │
|
|||
|
|
│ │ │ │ │→预览→配音│ │ │ 作品 │ │ │ │
|
|||
|
|
│ │ │ │ └──────────┘ │ │ │ │ │ │
|
|||
|
|
│ └──────┘ └──────────────┘ └──────┘ └──────┘ │
|
|||
|
|
│ ↑ ↑ │
|
|||
|
|
│ │ 读取客户DB 读取客户DB │
|
|||
|
|
└─────┼─────────────────────────────────┼────────────────────┘
|
|||
|
|
│ │
|
|||
|
|
┌─────┼─────────────────────────────────┼────────────────────┐
|
|||
|
|
│ │ 客户后端 │ │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ 客户DB ←──── Webhook回调 ←──── 乐读派后端 │
|
|||
|
|
│ (AI作品 (创作完成后实时推送) │
|
|||
|
|
│ + 自有作品) │
|
|||
|
|
└────────────────────────────────────────────────────────────┘
|
|||
|
|
|
|||
|
|
┌────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Android APK(乐读派打包提供) │
|
|||
|
|
│ 创作流程 → 完成 → Webhook回调 → 客户后端 │
|
|||
|
|
│ "我的作品" → 调客户提供的API │
|
|||
|
|
└────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 职责划分
|
|||
|
|
|
|||
|
|
| 功能 | 负责方 | 说明 |
|
|||
|
|
|------|--------|------|
|
|||
|
|
| AI 创作 H5 页面 | **乐读派** | 提供完整创作流程,客户 iframe 嵌入 |
|
|||
|
|
| AI 创作后端 API | **乐读派** | A6 角色提取、A3 故事创作、A20 配音等 |
|
|||
|
|
| Android APK | **乐读派** | 打包发布,客户提供"我的作品"接口即可 |
|
|||
|
|
| 广场 / 作品库 / 我的 | **客户** | 客户自有 H5 + 后端 |
|
|||
|
|
| 作品管理后台 | **客户** | 客户自有管理后台 |
|
|||
|
|
| Webhook 接收 | **客户** | 接收乐读派推送的创作结果 |
|
|||
|
|
| 数据存储 | **客户** | AI 作品数据存入客户自己的 DB |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、对接前准备
|
|||
|
|
|
|||
|
|
### 2.1 乐读派提供
|
|||
|
|
|
|||
|
|
以下信息由乐读派管理后台创建机构后生成,正式对接时填入:
|
|||
|
|
|
|||
|
|
| 项目 | 值 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| orgId(机构ID) | `__________` | 机构唯一标识,所有 API 调用和数据归属依据此 ID |
|
|||
|
|
| appSecret(机构密钥) | `__________` | API 认证密钥,**严禁泄露**,仅存于客户服务端 |
|
|||
|
|
| H5 创作页地址 | `__________` | 乐读派 H5 前端 URL(iframe src 用) |
|
|||
|
|
| API 服务地址 | `__________` | 乐读派后端 API 基地址 |
|
|||
|
|
| Android APK | 另行交付 | 已内置上述配置的签名发布包 |
|
|||
|
|
| 创作额度 | `__________` 次/周期 | 机构总创作额度(管理后台可调整) |
|
|||
|
|
|
|||
|
|
> **重要**:以上所有 `__________` 空白项将在正式开通机构后由乐读派填入并发送给客户。请勿使用测试值上线。
|
|||
|
|
|
|||
|
|
### 2.2 客户提供
|
|||
|
|
|
|||
|
|
| 项目 | 内容 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| Webhook 接收 URL | `https://客户域名/webhook/leai` | HTTPS,5 秒内返回 200 |
|
|||
|
|
| H5 嵌入域名 | `https://客户h5域名` | 用于 CORS 和 iframe 白名单 |
|
|||
|
|
| 机构查询接口(Android用) | `GET /api/org/by-device?mac=xx` | 根据设备MAC返回orgId(见 6.2) |
|
|||
|
|
| 我的作品接口(Android用) | `GET /api/my-works?orgId=xx&phone=xx` | 返回作品列表+详情(见 6.3) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、C端 H5 嵌入(iframe 方案)
|
|||
|
|
|
|||
|
|
### 3.1 嵌入原理
|
|||
|
|
|
|||
|
|
客户的"创作"Tab 内放一个 iframe,加载乐读派 H5 创作页面。创作完成后,乐读派 H5 通过 `postMessage` 通知客户父页面。
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
客户H5页面 乐读派H5(iframe内)
|
|||
|
|
│ │
|
|||
|
|
│ 1. 客户后端换取 sessionToken │
|
|||
|
|
│ │
|
|||
|
|
│ 2. iframe.src = 乐读派H5 │
|
|||
|
|
│ + token + orgId + phone │
|
|||
|
|
│ ──────────────────────────────→ │
|
|||
|
|
│ │
|
|||
|
|
│ 用户在iframe内创作... │
|
|||
|
|
│ │
|
|||
|
|
│ 3. 创作完成: postMessage │
|
|||
|
|
│ ←────────────────────────────── │
|
|||
|
|
│ {type:'WORK_CREATED', │
|
|||
|
|
│ workId:'xxx'} │
|
|||
|
|
│ │
|
|||
|
|
│ 4. 同时: Webhook推送到客户后端 │
|
|||
|
|
│ │
|
|||
|
|
│ 5. 客户刷新作品列表 │
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 客户 H5 嵌入代码(完整示例,可直接使用)
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<!-- 客户的"创作"Tab页面 -->
|
|||
|
|
<template>
|
|||
|
|
<div class="create-tab">
|
|||
|
|
<!-- 乐读派创作iframe -->
|
|||
|
|
<iframe
|
|||
|
|
v-if="iframeSrc"
|
|||
|
|
:src="iframeSrc"
|
|||
|
|
ref="creationFrame"
|
|||
|
|
class="creation-iframe"
|
|||
|
|
allow="camera;microphone"
|
|||
|
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
|
|||
|
|
/>
|
|||
|
|
<div v-else class="loading">正在加载创作工具...</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|||
|
|
import axios from 'axios'
|
|||
|
|
|
|||
|
|
// ★★★ 替换为乐读派提供的值(见第二章配置表)★★★
|
|||
|
|
const LEAI_H5_URL = '__________ /* 乐读派H5创作页地址 */'
|
|||
|
|
const CREATE_TOKEN_API = '/api/create-token' // 客户自己的后端接口(见3.3)
|
|||
|
|
// ★★★ 替换结束 ★★★
|
|||
|
|
|
|||
|
|
const iframeSrc = ref('')
|
|||
|
|
|
|||
|
|
onMounted(async () => {
|
|||
|
|
try {
|
|||
|
|
// 1. 调客户自己的后端,获取 sessionToken
|
|||
|
|
// 客户后端内部会调乐读派的 /api/v1/auth/session
|
|||
|
|
const { data } = await axios.post(CREATE_TOKEN_API, {
|
|||
|
|
phone: getCurrentUserPhone() // 从客户登录态获取当前用户手机号
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 2. 拼接 iframe URL
|
|||
|
|
iframeSrc.value = `${LEAI_H5_URL}/?token=${data.token}&orgId=${data.orgId}&phone=${data.phone}&embed=1`
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('获取创作令牌失败', e)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 监听 postMessage(创作完成通知)
|
|||
|
|
window.addEventListener('message', onCreationMessage)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
window.removeEventListener('message', onCreationMessage)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function onCreationMessage(event) {
|
|||
|
|
// 安全校验:只处理来自乐读派H5的消息
|
|||
|
|
if (!event.origin.includes('leai')) return
|
|||
|
|
const msg = event.data
|
|||
|
|
if (msg?.type === 'WORK_CREATED') {
|
|||
|
|
// 创作完成!workId 可用于跳转到作品详情
|
|||
|
|
console.log('新作品创建成功:', msg.workId)
|
|||
|
|
// 客户可以:刷新作品列表 / 跳转到作品Tab / 显示成功提示
|
|||
|
|
refreshMyWorks()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getCurrentUserPhone() {
|
|||
|
|
// ★ 替换为客户自己的获取当前登录用户手机号的逻辑
|
|||
|
|
return '13800001111'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function refreshMyWorks() {
|
|||
|
|
// ★ 替换为客户自己的刷新作品列表逻辑
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.create-tab {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
.creation-iframe {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
border: none;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 客户后端:令牌交换接口(完整示例)
|
|||
|
|
|
|||
|
|
客户后端需要实现一个接口,内部调用乐读派的令牌交换 API:
|
|||
|
|
|
|||
|
|
**Java (Spring Boot):**
|
|||
|
|
```java
|
|||
|
|
@RestController
|
|||
|
|
public class LeAiController {
|
|||
|
|
|
|||
|
|
// ★★★ 替换为乐读派提供的值(见第二章配置表)★★★
|
|||
|
|
private static final String LEAI_API = "__________"; // API服务地址
|
|||
|
|
private static final String ORG_ID = "__________"; // 机构ID
|
|||
|
|
private static final String APP_SECRET = "__________"; // 机构密钥
|
|||
|
|
|
|||
|
|
private final RestTemplate restTemplate = new RestTemplate();
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 客户前端调这个接口获取创作令牌
|
|||
|
|
* POST /api/create-token
|
|||
|
|
* Body: { "phone": "13800001111" }
|
|||
|
|
*/
|
|||
|
|
@PostMapping("/api/create-token")
|
|||
|
|
public Map<String, String> createToken(@RequestBody Map<String, String> req) {
|
|||
|
|
String phone = req.get("phone");
|
|||
|
|
|
|||
|
|
// 调乐读派令牌交换接口
|
|||
|
|
Map<String, String> body = new HashMap<>();
|
|||
|
|
body.put("orgId", ORG_ID);
|
|||
|
|
body.put("appSecret", APP_SECRET);
|
|||
|
|
body.put("phone", phone);
|
|||
|
|
|
|||
|
|
Map response = restTemplate.postForObject(
|
|||
|
|
LEAI_API + "/api/v1/auth/session", body, Map.class);
|
|||
|
|
Map data = (Map) response.get("data");
|
|||
|
|
|
|||
|
|
// 返回给前端
|
|||
|
|
Map<String, String> result = new HashMap<>();
|
|||
|
|
result.put("token", (String) data.get("sessionToken"));
|
|||
|
|
result.put("orgId", ORG_ID);
|
|||
|
|
result.put("phone", phone);
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Python (Flask):**
|
|||
|
|
```python
|
|||
|
|
import requests
|
|||
|
|
from flask import Flask, request, jsonify
|
|||
|
|
|
|||
|
|
LEAI_API = "__________" # ★ API服务地址(见第二章配置表)
|
|||
|
|
ORG_ID = "__________" # ★ 机构ID
|
|||
|
|
APP_SECRET = "__________" # ★ 机构密钥
|
|||
|
|
|
|||
|
|
app = Flask(__name__)
|
|||
|
|
|
|||
|
|
@app.route("/api/create-token", methods=["POST"])
|
|||
|
|
def create_token():
|
|||
|
|
phone = request.json["phone"]
|
|||
|
|
res = requests.post(f"{LEAI_API}/api/v1/auth/session", json={
|
|||
|
|
"orgId": ORG_ID, "appSecret": APP_SECRET, "phone": phone
|
|||
|
|
})
|
|||
|
|
token = res.json()["data"]["sessionToken"]
|
|||
|
|
return jsonify({"token": token, "orgId": ORG_ID, "phone": phone})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Node.js (Express):**
|
|||
|
|
```javascript
|
|||
|
|
const axios = require('axios')
|
|||
|
|
const LEAI_API = '__________' // ★ API服务地址(见第二章配置表)
|
|||
|
|
const ORG_ID = '__________' // ★ 机构ID
|
|||
|
|
const APP_SECRET = '__________' // ★ 机构密钥
|
|||
|
|
|
|||
|
|
app.post('/api/create-token', async (req, res) => {
|
|||
|
|
const { phone } = req.body
|
|||
|
|
const { data } = await axios.post(`${LEAI_API}/api/v1/auth/session`, {
|
|||
|
|
orgId: ORG_ID, appSecret: APP_SECRET, phone
|
|||
|
|
})
|
|||
|
|
res.json({ token: data.data.sessionToken, orgId: ORG_ID, phone })
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.4 iframe 嵌入注意事项
|
|||
|
|
|
|||
|
|
| 事项 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| CORS 白名单 | 联系乐读派将客户 H5 域名加入 `allowed_origins` |
|
|||
|
|
| HTTPS 必须 | iframe 父页面和乐读派 H5 都必须是 HTTPS |
|
|||
|
|
| `embed=1` 参数 | 告诉乐读派 H5 处于嵌入模式(隐藏返回按钮等) |
|
|||
|
|
| Token 有效期 | 2 小时,建议每次打开创作Tab时重新获取 |
|
|||
|
|
| 相机权限 | iframe 需要 `allow="camera"` 属性才能拍照上传 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、Webhook 数据同步
|
|||
|
|
|
|||
|
|
### 4.1 同步机制全景图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户在iframe中创作
|
|||
|
|
│
|
|||
|
|
↓
|
|||
|
|
乐读派后端完成AI生成
|
|||
|
|
│
|
|||
|
|
├──→ postMessage通知iframe父页面(即时,用于前端刷新)
|
|||
|
|
│
|
|||
|
|
└──→ Webhook POST到客户后端(1-3秒,用于数据持久化)
|
|||
|
|
│
|
|||
|
|
↓
|
|||
|
|
客户后端接收
|
|||
|
|
│
|
|||
|
|
├── 验签(确认来自乐读派)
|
|||
|
|
├── 解析作品数据(标题/图片/音频/文字)
|
|||
|
|
├── 存入客户DB(供 广场/作品库 使用)
|
|||
|
|
└── 返回200
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 客户需要实现的 Webhook 接口
|
|||
|
|
|
|||
|
|
**只需要一个 POST 端点:**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
POST https://客户域名/webhook/leai
|
|||
|
|
Content-Type: application/json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**完整实现示例 (Java Spring Boot):**
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
import javax.crypto.Mac;
|
|||
|
|
import javax.crypto.spec.SecretKeySpec;
|
|||
|
|
import org.apache.commons.codec.binary.Hex;
|
|||
|
|
import java.security.MessageDigest;
|
|||
|
|
|
|||
|
|
@RestController
|
|||
|
|
public class WebhookController {
|
|||
|
|
|
|||
|
|
private static final String APP_SECRET = "__________"; // ★ 机构密钥(见第二章配置表)
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 接收乐读派Webhook回调
|
|||
|
|
* 所有事件类型都走这一个接口
|
|||
|
|
*/
|
|||
|
|
@PostMapping("/webhook/leai")
|
|||
|
|
public Map<String, String> handleWebhook(
|
|||
|
|
@RequestBody String rawBody,
|
|||
|
|
@RequestHeader("X-Webhook-Id") String webhookId,
|
|||
|
|
@RequestHeader("X-Webhook-Timestamp") String timestamp,
|
|||
|
|
@RequestHeader("X-Webhook-Signature") String signatureHeader) {
|
|||
|
|
|
|||
|
|
// 1. 时间窗口检查(防重放,5分钟有效)
|
|||
|
|
long ts = Long.parseLong(timestamp);
|
|||
|
|
if (Math.abs(System.currentTimeMillis() - ts) > 300_000) {
|
|||
|
|
return Map.of("error", "expired");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 验证签名
|
|||
|
|
String signData = webhookId + "." + timestamp + "." + rawBody;
|
|||
|
|
Mac mac = Mac.getInstance("HmacSHA256");
|
|||
|
|
mac.init(new SecretKeySpec(APP_SECRET.getBytes("UTF-8"), "HmacSHA256"));
|
|||
|
|
String expected = "HMAC-SHA256=" + Hex.encodeHexString(
|
|||
|
|
mac.doFinal(signData.getBytes("UTF-8")));
|
|||
|
|
if (!MessageDigest.isEqual(expected.getBytes(), signatureHeader.getBytes())) {
|
|||
|
|
return Map.of("error", "invalid signature");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 解析事件
|
|||
|
|
JSONObject payload = JSON.parseObject(rawBody);
|
|||
|
|
String eventId = payload.getString("id");
|
|||
|
|
String event = payload.getString("event");
|
|||
|
|
JSONObject data = payload.getJSONObject("data");
|
|||
|
|
|
|||
|
|
// 4. 幂等去重(用eventId判断是否已处理)
|
|||
|
|
if (isProcessed(eventId)) {
|
|||
|
|
return Map.of("status", "duplicate");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. 按事件类型处理
|
|||
|
|
switch (event) {
|
|||
|
|
case "work.completed":
|
|||
|
|
handleWorkCompleted(data);
|
|||
|
|
break;
|
|||
|
|
case "work.updated":
|
|||
|
|
handleWorkUpdated(data);
|
|||
|
|
break;
|
|||
|
|
case "work.audio_updated":
|
|||
|
|
handleAudioUpdated(data);
|
|||
|
|
break;
|
|||
|
|
case "work.failed":
|
|||
|
|
handleWorkFailed(data);
|
|||
|
|
break;
|
|||
|
|
// 其他事件按需处理
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
markProcessed(eventId);
|
|||
|
|
return Map.of("status", "ok");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 作品创作完成 — 最重要的事件
|
|||
|
|
* 存入客户DB,供广场和作品库使用
|
|||
|
|
*/
|
|||
|
|
private void handleWorkCompleted(JSONObject data) {
|
|||
|
|
String workId = data.getString("work_id");
|
|||
|
|
String title = data.getString("title");
|
|||
|
|
String author = data.getString("author");
|
|||
|
|
String phone = data.getString("phone"); // 创作者手机号
|
|||
|
|
String style = data.getString("style");
|
|||
|
|
int completionStep = data.getIntValue("completion_step");
|
|||
|
|
int dataVersion = data.getIntValue("data_version");
|
|||
|
|
JSONArray pageList = data.getJSONArray("page_list");
|
|||
|
|
|
|||
|
|
// ★ 存入客户自己的作品表
|
|||
|
|
// dataVersion门卫:只有新版本才更新
|
|||
|
|
MyWork local = myWorkRepository.findByWorkId(workId);
|
|||
|
|
if (local != null && dataVersion <= local.getDataVersion()) {
|
|||
|
|
return; // 旧数据,跳过
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
MyWork work = local != null ? local : new MyWork();
|
|||
|
|
work.setWorkId(workId);
|
|||
|
|
work.setTitle(title);
|
|||
|
|
work.setAuthor(author);
|
|||
|
|
work.setPhone(phone);
|
|||
|
|
work.setStyle(style);
|
|||
|
|
work.setStatus("COMPLETED");
|
|||
|
|
work.setCompletionStep(completionStep);
|
|||
|
|
work.setDataVersion(dataVersion);
|
|||
|
|
work.setSource("AI_CREATION"); // 标记来源:AI创作(区别于客户自有作品)
|
|||
|
|
|
|||
|
|
// 存储页面数据
|
|||
|
|
if (pageList != null) {
|
|||
|
|
work.setPageListJson(pageList.toJSONString());
|
|||
|
|
// 封面图(第一页的图片URL)
|
|||
|
|
if (pageList.size() > 0) {
|
|||
|
|
work.setCoverUrl(pageList.getJSONObject(0).getString("image_url"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
myWorkRepository.save(work);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 Webhook 回调数据格式
|
|||
|
|
|
|||
|
|
**请求头:**
|
|||
|
|
```
|
|||
|
|
X-Webhook-Id: evt_1912345678901234567
|
|||
|
|
X-Webhook-Timestamp: 1712000000000
|
|||
|
|
X-Webhook-Signature: HMAC-SHA256=a3f8c2d1e5b7...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**请求体(work.completed 事件):**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": "evt_1912345678901234567",
|
|||
|
|
"event": "work.completed",
|
|||
|
|
"created_at": 1712000000000,
|
|||
|
|
"data": {
|
|||
|
|
"work_id": "1912345678901234567",
|
|||
|
|
"org_id": "ORG001",
|
|||
|
|
"status": "COMPLETED",
|
|||
|
|
"title": "小兔子的冒险",
|
|||
|
|
"author": "小明",
|
|||
|
|
"phone": "13800001111",
|
|||
|
|
"style": "watercolor",
|
|||
|
|
"pages": 6,
|
|||
|
|
"completion_step": 0,
|
|||
|
|
"data_version": 1,
|
|||
|
|
"page_list": [
|
|||
|
|
{"page_num": 0, "text": "小兔子的冒险", "image_url": "https://oss.../p0.png", "audio_url": null},
|
|||
|
|
{"page_num": 1, "text": "在一个阳光明媚的早晨...", "image_url": "https://oss.../p1.png", "audio_url": null}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.4 客户 DB 建表参考
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE my_works (
|
|||
|
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|||
|
|
work_id VARCHAR(50) UNIQUE NOT NULL, -- 乐读派作品ID
|
|||
|
|
|
|||
|
|
-- 乐读派同步字段(Webhook写入,不要手动改)
|
|||
|
|
title VARCHAR(200),
|
|||
|
|
author VARCHAR(50),
|
|||
|
|
phone VARCHAR(20), -- 创作者
|
|||
|
|
status VARCHAR(20), -- COMPLETED/FAILED
|
|||
|
|
style VARCHAR(50),
|
|||
|
|
completion_step INT DEFAULT 0,
|
|||
|
|
data_version INT NOT NULL DEFAULT 0, -- ★ 同步对比用
|
|||
|
|
page_list_json MEDIUMTEXT, -- 页面JSON
|
|||
|
|
cover_url VARCHAR(500), -- 封面图URL
|
|||
|
|
|
|||
|
|
-- 客户自有字段
|
|||
|
|
source VARCHAR(20) DEFAULT 'AI_CREATION', -- AI_CREATION=AI创作 / USER_UPLOAD=用户自传
|
|||
|
|
is_featured TINYINT DEFAULT 0, -- 是否精选(广场展示)
|
|||
|
|
review_status VARCHAR(20) DEFAULT 'PENDING', -- 审核状态
|
|||
|
|
user_id BIGINT, -- 客户系统的用户ID(通过phone关联)
|
|||
|
|
|
|||
|
|
created_at DATETIME DEFAULT NOW(),
|
|||
|
|
updated_at DATETIME DEFAULT NOW() ON UPDATE NOW(),
|
|||
|
|
INDEX idx_phone (phone),
|
|||
|
|
INDEX idx_source (source),
|
|||
|
|
INDEX idx_featured (is_featured)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.5 签名验证依赖
|
|||
|
|
|
|||
|
|
Java 项目需要添加 Apache Commons Codec:
|
|||
|
|
|
|||
|
|
```xml
|
|||
|
|
<dependency>
|
|||
|
|
<groupId>commons-codec</groupId>
|
|||
|
|
<artifactId>commons-codec</artifactId>
|
|||
|
|
<version>1.16.0</version>
|
|||
|
|
</dependency>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、postMessage 通信协议
|
|||
|
|
|
|||
|
|
### 5.1 乐读派 H5 → 客户父页面
|
|||
|
|
|
|||
|
|
当用户在 iframe 中完成创作流程,乐读派 H5 会通过 `window.parent.postMessage` 发送以下消息:
|
|||
|
|
|
|||
|
|
| 事件 | 触发时机 | 数据 |
|
|||
|
|
|------|---------|------|
|
|||
|
|
| `WORK_CREATED` | 作品创建成功(A3提交后) | `{type:'WORK_CREATED', workId:'xxx'}` |
|
|||
|
|
| `WORK_COMPLETED` | 创作完成(图文生成完毕) | `{type:'WORK_COMPLETED', workId:'xxx'}` |
|
|||
|
|
| `CREATION_ERROR` | 创作失败 | `{type:'CREATION_ERROR', message:'xxx'}` |
|
|||
|
|
|
|||
|
|
### 5.2 客户父页面监听示例
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
window.addEventListener('message', (event) => {
|
|||
|
|
// 安全校验
|
|||
|
|
if (!event.origin.includes('leai域名')) return
|
|||
|
|
|
|||
|
|
const { type, workId } = event.data
|
|||
|
|
switch (type) {
|
|||
|
|
case 'WORK_COMPLETED':
|
|||
|
|
// 创作完成,可以:
|
|||
|
|
// 1. 切换到"作品"Tab
|
|||
|
|
// 2. 刷新作品列表
|
|||
|
|
// 3. 显示成功提示
|
|||
|
|
showToast('创作完成!')
|
|||
|
|
switchToWorksTab()
|
|||
|
|
break
|
|||
|
|
case 'CREATION_ERROR':
|
|||
|
|
showToast('创作失败:' + event.data.message)
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> **注意**: postMessage 用于前端即时通知(告诉客户H5"创作完了")。完整的作品数据通过 Webhook 异步推送到客户后端,客户前端从自己的后端获取。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、Android APK 对接
|
|||
|
|
|
|||
|
|
### 6.1 交付方式
|
|||
|
|
|
|||
|
|
乐读派打包签名的 APK 交付给客户。客户**不需要**源代码。
|
|||
|
|
|
|||
|
|
APK 中**不写死机构ID**,而是通过客户提供的接口动态获取(见 6.2)。
|
|||
|
|
|
|||
|
|
打包时,客户需提供以下信息(乐读派代入配置):
|
|||
|
|
|
|||
|
|
| 配置项 | 示例 | 说明 |
|
|||
|
|
|--------|------|------|
|
|||
|
|
| 乐读派 API 地址 | `__________` | 乐读派后端(见第二章) |
|
|||
|
|
| 机构密钥 | `__________` | 客户的 appSecret(见第二章) |
|
|||
|
|
| 客户 API 基地址 | `https://客户域名/api` | 用于调 6.2/6.3 的接口 |
|
|||
|
|
|
|||
|
|
### 6.2 客户需提供的接口①:获取机构ID
|
|||
|
|
|
|||
|
|
Android 端启动时,通过设备 MAC 地址向客户后端查询所属机构。**机构ID 不写死在 APK 中**,支持同一 APK 部署到不同机构的设备。
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET https://客户域名/api/org/by-device?mac={设备MAC地址}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
响应格式:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"data": {
|
|||
|
|
"orgId": "ORG001",
|
|||
|
|
"orgName": "XX教育机构"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| 字段 | 类型 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| orgId | String | 乐读派分配的机构ID(必须与第二章配置表一致) |
|
|||
|
|
| orgName | String | 机构名称(可选,用于 Android 端显示) |
|
|||
|
|
|
|||
|
|
> **流程**:Android 启动 → 读取设备 MAC → 调客户接口获取 orgId → 后续所有 API 调用使用该 orgId。
|
|||
|
|
|
|||
|
|
### 6.3 客户需提供的接口②:我的作品
|
|||
|
|
|
|||
|
|
Android 端"作品"Tab 展示当前用户在该机构下的作品列表。使用 **orgId + phone** 组合查询。
|
|||
|
|
|
|||
|
|
**作品列表:**
|
|||
|
|
```
|
|||
|
|
GET https://客户域名/api/my-works?orgId={orgId}&phone={phone}&page=1&size=20
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
响应格式(**字段名固定,乐读派 Android 端直接解析**):
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"data": {
|
|||
|
|
"total": 42,
|
|||
|
|
"records": [
|
|||
|
|
{
|
|||
|
|
"workId": "1912345678901234567",
|
|||
|
|
"title": "小兔子的冒险",
|
|||
|
|
"coverUrl": "https://oss.../p0.png",
|
|||
|
|
"status": "COMPLETED",
|
|||
|
|
"createdAt": "2026-04-03 10:30:00"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**作品详情:**
|
|||
|
|
```
|
|||
|
|
GET https://客户域名/api/my-works/{workId}?orgId={orgId}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
响应格式:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"data": {
|
|||
|
|
"workId": "1912345678901234567",
|
|||
|
|
"title": "小兔子的冒险",
|
|||
|
|
"author": "小明",
|
|||
|
|
"pageList": [
|
|||
|
|
{"pageNum": 0, "text": "封面", "imageUrl": "https://...", "audioUrl": null},
|
|||
|
|
{"pageNum": 1, "text": "故事内容...", "imageUrl": "https://...", "audioUrl": "https://..."}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> **注意**:
|
|||
|
|
> - 字段名使用 **camelCase**(如果客户 DB 存的是 Webhook 的 snake_case,需在接口层转换)
|
|||
|
|
> - `orgId` 必填,用于隔离不同机构的数据
|
|||
|
|
> - `phone` 来自用户登录,Android 端自动携带
|
|||
|
|
|
|||
|
|
### 6.4 Android 端数据流
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Android 启动
|
|||
|
|
│
|
|||
|
|
├── 读取设备MAC地址
|
|||
|
|
│
|
|||
|
|
├── GET /api/org/by-device?mac=xx:xx:xx → 获取 orgId
|
|||
|
|
│
|
|||
|
|
├── 用户登录 → 获取 phone
|
|||
|
|
│
|
|||
|
|
├── 创作流程 → 调乐读派API(orgId + appSecret + phone)
|
|||
|
|
│ │
|
|||
|
|
│ └── 创作完成 → Webhook推送到客户后端
|
|||
|
|
│
|
|||
|
|
└── "我的作品" → GET /api/my-works?orgId=xx&phone=xx → 客户后端返回
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、数据流全景与同步时序
|
|||
|
|
|
|||
|
|
### 7.1 用户创作一个作品的完整数据流
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
时间轴 →
|
|||
|
|
|
|||
|
|
用户操作 客户H5 乐读派H5(iframe) 乐读派后端 客户后端
|
|||
|
|
│ │ │ │ │
|
|||
|
|
│ 点击"创作" │ │ │ │
|
|||
|
|
│ ──────────→ │ │ │ │
|
|||
|
|
│ │ 换取token │ │ │
|
|||
|
|
│ │ ──────────────────────────────→ │ │
|
|||
|
|
│ │ ←─ sessionToken ────────────── │ │
|
|||
|
|
│ │ │ │ │
|
|||
|
|
│ │ 加载iframe │ │ │
|
|||
|
|
│ │ ──────────→ │ │ │
|
|||
|
|
│ │ │ │ │
|
|||
|
|
│ 拍照上传 │ │ A6角色提取 │ │
|
|||
|
|
│ ──────────────────────────→ │ ────────────→ │ │
|
|||
|
|
│ │ │ ←── 角色列表 ── │ │
|
|||
|
|
│ │ │ │ │
|
|||
|
|
│ 选画风+写故事 │ │ A3创作 │ │
|
|||
|
|
│ ──────────────────────────→ │ ────────────→ │ │
|
|||
|
|
│ │ │ │ AI生成中... │
|
|||
|
|
│ │ │ ←── 进度更新 ── │ │
|
|||
|
|
│ │ │ │ │
|
|||
|
|
│ │ │ ←── 创作完成 ── │ │
|
|||
|
|
│ │ ← postMessage │ │ │
|
|||
|
|
│ │ WORK_COMPLETED │ │ │
|
|||
|
|
│ │ │ │ Webhook POST │
|
|||
|
|
│ │ │ │ ────────────→ │
|
|||
|
|
│ │ │ │ │ 验签+存DB
|
|||
|
|
│ │ │ │ ← 200 ────── │
|
|||
|
|
│ 看到"创作完成" │ │ │ │
|
|||
|
|
│ │ 刷新作品列表 │ │ │
|
|||
|
|
│ │ (从客户后端取) │ │ │
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.2 数据同步保障
|
|||
|
|
|
|||
|
|
| 层级 | 机制 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 实时通知 | postMessage | iframe 创作完成后立即通知客户 H5 前端 |
|
|||
|
|
| 数据同步 | Webhook | 创作完成后 1-3 秒推送到客户后端 |
|
|||
|
|
| 重试保障 | 自动重试 5 次 | 10s/30s/2m/10m/30m,确保数据不丢 |
|
|||
|
|
| 兜底对账 | B3 定时查询 | 建议每 5 分钟查一次,对比 data_version |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、对接验证清单
|
|||
|
|
|
|||
|
|
按顺序逐步验证,每步都通过后再进行下一步:
|
|||
|
|
|
|||
|
|
### Phase 1: 后端连通(1天)
|
|||
|
|
|
|||
|
|
- [ ] 收到乐读派提供的 orgId + appSecret
|
|||
|
|
- [ ] 调用令牌交换接口成功:`POST /api/v1/auth/session`
|
|||
|
|
- [ ] 实现 Webhook 接收端点:`POST /webhook/leai`
|
|||
|
|
- [ ] 管理后台配置回调 URL + 测试连通
|
|||
|
|
|
|||
|
|
### Phase 2: iframe 嵌入(1天)
|
|||
|
|
|
|||
|
|
- [ ] 客户 H5 域名加入 CORS 白名单(联系乐读派)
|
|||
|
|
- [ ] iframe 加载乐读派 H5 正常显示
|
|||
|
|
- [ ] iframe 内可拍照/选图上传
|
|||
|
|
- [ ] iframe 内完整创作流程走通(上传→提取→画风→创作→预览)
|
|||
|
|
|
|||
|
|
### Phase 3: 数据同步(1天)
|
|||
|
|
|
|||
|
|
- [ ] Webhook 收到 `work.completed` 事件
|
|||
|
|
- [ ] 签名验证通过
|
|||
|
|
- [ ] 作品数据正确写入客户 DB
|
|||
|
|
- [ ] 客户"作品库"能展示 AI 创作的作品
|
|||
|
|
- [ ] postMessage 通知正常接收
|
|||
|
|
|
|||
|
|
### Phase 4: Android 交付(1天)
|
|||
|
|
|
|||
|
|
- [ ] 客户提供"我的作品"API 接口文档
|
|||
|
|
- [ ] 乐读派打包 APK 配置客户参数
|
|||
|
|
- [ ] APK 安装后创作流程正常
|
|||
|
|
- [ ] "我的作品"展示客户接口返回的数据
|
|||
|
|
- [ ] Webhook 正常推送
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、常见问题
|
|||
|
|
|
|||
|
|
**Q: iframe 内创作完成后,客户怎么知道?**
|
|||
|
|
A: 两个通道同时通知:① postMessage 即时通知客户前端(用于刷新UI);② Webhook 异步推送到客户后端(用于持久化数据)。
|
|||
|
|
|
|||
|
|
**Q: 客户的"广场"数据怎么来?**
|
|||
|
|
A: 所有 AI 作品通过 Webhook 同步到客户 DB 后,客户在管理后台标记"精选",广场从客户 DB 读取 `is_featured=1` 的作品展示。
|
|||
|
|
|
|||
|
|
**Q: 用户在 iframe 创作时网络断了怎么办?**
|
|||
|
|
A: 创作请求已提交到乐读派后端的不受影响(后端异步生成)。Webhook 会在创作完成后推送。如果用户关闭了页面,下次打开"作品库"也能看到已完成的作品。
|
|||
|
|
|
|||
|
|
**Q: Token 过期了怎么办?**
|
|||
|
|
A: 每次用户打开"创作"Tab 时重新获取 Token(2小时有效)。创作过程中 Token 过期不影响已提交的创作任务。
|
|||
|
|
|
|||
|
|
**Q: 客户想修改创作 UI 怎么办?**
|
|||
|
|
A: 联系乐读派,我们修改 H5 代码后重新部署。客户不需要改任何代码,iframe 自动加载最新版本。
|
|||
|
|
|
|||
|
|
**Q: OSS 图片 URL 会过期吗?**
|
|||
|
|
A: 不会。图片存储在乐读派 OSS,URL 永久有效(除非作品被删除)。客户可以直接在广场/作品库中使用这些 URL。
|
|||
|
|
|
|||
|
|
**Q: Android 端需要热更新怎么办?**
|
|||
|
|
A: 目前需要重新打包 APK。创作流程的 UI/逻辑更新需乐读派重新打包后交付。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 附录: 错误码速查
|
|||
|
|
|
|||
|
|
| 错误码 | 说明 | 处理 |
|
|||
|
|
|--------|------|------|
|
|||
|
|
| 200 | 成功 | - |
|
|||
|
|
| 10006 | 请求过于频繁 | 降低频率 |
|
|||
|
|
| 20002 | 账号锁定(5次密钥错误) | 等10分钟 |
|
|||
|
|
| 20010 | 会话令牌无效/过期 | 重新换取 token |
|
|||
|
|
| 30001 | 机构不存在 | 检查 orgId |
|
|||
|
|
| 30002 | 机构未授权 | 联系乐读派 |
|
|||
|
|
| 30003 | 创作额度不足 | 联系乐读派充值 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
> 乐读派 AI 绘本创作系统 | 企业定制对接指南 V3.1 | 2026-04-03
|