library-picturebook-activity/lesingle-aicreate-backend-demo/企业同步创作数据_核心三步_V4.0(1).md
En 98e9ad1d28 feat(前端): 测试环境登录框支持自动填充测试账号
通过 VITE_AUTO_FILL_TEST 环境变量控制,在 .env.test 中启用,
使测试环境构建后登录框也能自动填充测试账号,方便测试人员使用。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 17:03:22 +08:00

384 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 企业同步创作数据 — 核心三步
> 基于《AI绘本创作系统 企业后端集成指南 V4.0》,提炼企业同步乐读派创作数据的核心实现路径。
>
> 三层防线共用同一套同步判断规则,区别仅在于触发方式和时机。
---
## 前置知识5步状态机与同步判断规则
### 状态定义
| 数值 | 状态名 | 含义 |
|------|--------|------|
| -1 | FAILED | 创作失败(异常状态) |
| 1 | PENDING | 已提交,排队等待 |
| 2 | PROCESSING | AI创作中进度变化 |
| 3 | COMPLETED | 图片完成,待编目 |
| 4 | CATALOGED | 编目完成,待配音 |
| 5 | DUBBED | 配音完成(乐读派终态) |
正常流转:`1 → 2 → 3 → 4 → 5`,严格单向递增,不可回退。
### 统一同步判断规则(三步通用)
```
if (本地无记录) → INSERT新作品
if (remote_status == -1) → 强制UPDATE失败通知无条件处理
if (remote_status == 2) → 强制UPDATE进度变化无条件更新
if (remote_status > local_status) → 全量UPDATE状态前进
其他 → SKIP旧数据/重复,忽略)
```
> **重点**PROCESSING(2) 和 FAILED(-1) 是特殊状态,**不参与数值大小比较**,收到即强制更新。
---
## 第一步Webhook 回调推送(实时主通道)
### 作用
乐读派后端在作品状态变更时,**主动** POST 推送到企业配置的 Webhook URL实现秒级数据同步。
### 触发时机
每次 status 发生变化时自动推送,包括:
- 状态前进1→2、2→3、3→4、4→5
- 创作失败any→-1
### 两种事件类型
| 事件 | 触发时机 | 频率 | 说明 |
|------|----------|------|------|
| `work.status_changed` | 状态变更 | 每次状态变更1次 | 核心事件,携带完整作品数据(含 page_list |
| `work.progress` | PROCESSING 阶段进度变化 | 多次/作品 | 进度里程碑推送10%/30%/50%/70%/90% |
### Webhook 请求格式
```
POST {企业webhook_url}
Content-Type: application/json
X-Webhook-Id: evt_190368671438289
X-Webhook-Event: work.status_changed
X-Webhook-Timestamp: 1712000000000
X-Webhook-Signature: HMAC-SHA256=a3f8c2d1...
```
### 企业处理逻辑Java 伪代码)
```java
@Transactional
public void handleWebhook(JSONObject data) {
String workId = data.getString("work_id");
int remoteStatus = data.getIntValue("status");
// 1. 幂等去重(用 event_id防止重复处理
if (processedEvents.contains(eventId)) return;
// 2. 查本地记录(建议 SELECT ... FOR UPDATE 行锁)
WorkRecord local = db.selectForUpdate(workId);
// 3. 新作品 → 直接入库
if (local == null) {
db.insert(buildRecord(data));
return;
}
// 4. FAILED(-1) → 强制更新,无条件
if (remoteStatus == -1) {
local.setStatus(-1);
local.setFailReason(data.getString("fail_reason"));
db.update(local);
return;
}
// 5. PROCESSING(2) → 强制更新进度,无条件
if (remoteStatus == 2) {
local.setProgress(data.getIntValue("progress"));
local.setProgressMessage(data.getString("progress_message"));
db.update(local);
return;
}
// 6. 状态前进 → 全量覆盖
if (remoteStatus > local.getStatus()) {
updateAllFields(local, data); // 更新 title/author/page_list 等
db.update(local);
return;
}
// 7. 旧数据/重复 → 忽略
log.info("skip: remote={} <= local={}", remoteStatus, local.getStatus());
}
```
### 各状态下企业应保存的关键数据
| 状态 | 企业需保存的字段 | 用途 |
|------|--------------|------|
| 1 PENDING | work_id, phone, org_id, style, original_image_url | 创建本地记录,关联企业用户 |
| 2 PROCESSING | progress, progress_message | 显示创作进度(可选) |
| 3 COMPLETED | title, pages, page_list含 image_url | 作品图片已生成,可预览展示 |
| 4 CATALOGED | title, author, subtitle, intro, tags | 用户填写的编目信息 |
| 5 DUBBED | page_list含 audio_url | 配音URL已填充作品完整可用 |
| -1 FAILED | fail_reason | 记录失败原因,通知用户 |
### 签名验证(必须实现)
```
签名体 = "{X-Webhook-Id}.{X-Webhook-Timestamp}.{请求body原文}"
期望签名 = HMAC-SHA256(签名体, app_secret).toHex()
```
安全检查清单:
- **时间窗口**`|当前时间 - X-Webhook-Timestamp| ≤ 5分钟`,防重放
- **幂等去重**:用 `X-Webhook-Id` 记录已处理事件
- **常量时间比较**:用 `MessageDigest.isEqual()` 而非 `.equals()`,防时序攻击
- **原始body**:签名时使用 HTTP body 原文,不要反序列化再序列化
### 重试策略
首次发送 + 5次重试 = 共6次尝试。
延迟间隔:`10s → 30s → 2min → 10min → 30min → 30min`
全部失败后需通过**第二步 B3 批量拉取**兜底。
---
## 第二步B3 批量查询兜底(定时对账)
### 作用
Webhook 可能因网络问题、企业服务宕机等原因丢失。B3 定时批量拉取作为兜底通道,确保数据**最终一致**。
### 接口信息
```
GET /api/v1/query/works?orgId=ORG001&updatedAfter=2026-04-05T00:00:00
认证方式HMAC-SHA256 签名
```
返回指定时间之后有变更的所有作品列表。
### 建议配置
| 配置项 | 推荐值 | 说明 |
|--------|--------|------|
| 对账频率 | 每 30 分钟 | 不低于 15 分钟,避免对 API 造成查询压力 |
| 查询范围 | 最近 2 小时 | 覆盖 2 个对账周期,防止边界遗漏 |
### 企业实现伪代码Java
```java
@Scheduled(fixedRate = 30 * 60 * 1000) // 每30分钟执行
public void reconcile() {
// 查询最近2小时内有变更的作品
String since = twoHoursAgo.format(ISO_FORMAT);
List<RemoteWork> remoteList = callB3(since);
for (RemoteWork remote : remoteList) {
WorkRecord local = db.get(remote.workId);
// 同步判断规则与 Webhook 完全一致
if (local == null
|| remote.status > local.getStatus()
|| remote.status == 2
|| remote.status == -1) {
db.upsert(remote);
}
}
lastSyncTime = now();
}
```
### B3 调用签名示例
```java
Map<String, String> params = new LinkedHashMap<>();
params.put("orgId", ORG_ID);
params.put("updatedAfter", "2026-04-05T00:00:00");
// 生成 HMAC 签名头4个 Header
Map<String, String> headers = buildHmacHeaders(params, ORG_ID, APP_SECRET);
String url = API_URL + "/api/v1/query/works?orgId=" + ORG_ID
+ "&updatedAfter=" + URLEncoder.encode("2026-04-05T00:00:00", "UTF-8");
// 注意:传 URI 对象,避免 RestTemplate 双重编码
restTemplate.getForObject(URI.create(url), String.class);
```
### 并发安全Webhook + B3 同时触发)
推荐两种方案防止同一作品被并发更新:
```sql
-- 方案1: 行锁(推荐,简单可靠)
SELECT * FROM enterprise_work WHERE work_id = ? FOR UPDATE;
-- 然后按同步规则判断和更新
-- 方案2: CAS 乐观锁(无锁,适合高并发)
UPDATE enterprise_work
SET status = #{remoteStatus}, title = #{title}, page_list = #{pageList}
WHERE work_id = #{workId}
AND (status < #{remoteStatus} OR #{remoteStatus} = 2 OR #{remoteStatus} = -1);
-- rows=0 表示被其他线程抢先更新了,安全忽略
```
---
## 第三步:详情页进入时强制 B2 拉取(用户触发兜底)
### 作用
当用户点击进入作品详情页,且本地状态尚未完成(`status < 3`企业**强制调用 B2 单条查询**拉取最新数据确保用户看到的是最新状态
### 为什么需要这一步
- status 1/2 期间状态变化最快排队 创作中 完成 Webhook 丢失的**高风险窗口**
- B3 对账有 30 分钟延迟用户可能在此期间打开详情页
- 用户主动触发**即时补偿**体验最佳
### 触发条件
```
用户点击作品详情页 && local_status < 3
```
`status >= 3` 后变化由用户主动操作驱动编目/配音Webhook + B3 已足够覆盖无需额外拉取
### 接口信息
```
GET /api/v1/query/work/{workId}?orgId=ORG001
认证方式HMAC-SHA256 签名 或 Session Token
```
### 企业实现伪代码Java
```java
/**
* 用户点击作品详情页时调用
*/
public WorkRecord getWorkDetail(String workId) {
WorkRecord local = db.get(workId);
// 本地状态 < 3PENDING 或 PROCESSING强制从乐读派拉取最新
if (local != null && local.getStatus() < 3) {
RemoteWork remote = callB2(workId);
// 同步判断规则与 Webhook、B3 完全一致
if (remote.status > local.getStatus()
|| remote.status == 2
|| remote.status == -1) {
updateAllFields(local, remote);
db.update(local);
}
}
// 返回最新的本地记录,渲染详情页
return db.get(workId);
}
```
### 前端配合(可选)
如果企业有自建 C 可在作品列表页面根据 status 做路由跳转
```javascript
switch (work.status) {
case 1: case 2: // 排队/创作中
navigate('/creating/' + workId); // → 进度等待页
break;
case 3: // 图片完成
navigate('/catalog/' + workId); // → 编目编辑页(强制)
break;
case 4: // 编目完成
navigate('/dubbing/' + workId); // → 配音编辑页(强制)
break;
case 5: // 配音完成(终态)
navigate('/reader/' + workId); // → 阅读页
break;
case -1: // 失败
showError(work.failReason);
break;
}
```
---
## 三步协同总览
```
┌──────────────────────────────────────────────────────────────────┐
│ 企业同步三层防线 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 第一步 Webhook 推送 秒级实时 ← 主通道(可能丢失) │
│ ↓ 漏网之鱼 │
│ 第二步 B3 定时对账 30分钟兜底 ← 保障通道(高可靠) │
│ ↓ 用户等不及 │
│ 第三步 B2 详情页强制拉取 用户触发 ← 即时补偿status<3时
│ │
├──────────────────────────────────────────────────────────────────┤
│ 三层共用统一同步规则: │
│ · remote > local → 全量更新 │
│ · remote == 2 (进度) → 强制更新 │
│ · remote == -1 (失败) → 强制更新 │
│ · 其他 → 忽略 │
└──────────────────────────────────────────────────────────────────┘
```
### 数据流对比
| 维度 | 第一步 Webhook | 第二步 B3 对账 | 第三步 B2 强制拉取 |
|------|-------------|------------|---------------|
| 触发方式 | 乐读派主动推送 | 企业定时轮询 | 用户打开详情页 |
| 实时性 | 秒级 | 30分钟级 | 即时 |
| 可靠性 | 可能丢失 | 高可靠 | 高可靠 |
| 触发条件 | 状态变更时自动 | 定时任务 | `local_status < 3` |
| 数据范围 | 单条作品 | 时间范围内批量 | 单条作品 |
| 认证方式 | 签名验证被动接收 | HMAC 签名主动请求 | HMAC/Token主动请求 |
---
## 企业推荐表结构
```sql
CREATE TABLE `enterprise_work` (
`work_id` VARCHAR(32) PRIMARY KEY,
`status` INT NOT NULL DEFAULT 0 COMMENT '状态: 乐读派1-5, 企业>=6',
`progress` INT DEFAULT 0 COMMENT '创作进度0-100',
`title` VARCHAR(200),
`author` VARCHAR(50),
`tags` JSON,
`page_list` JSON COMMENT '页面数据含image_url/audio_url',
`original_image_url` VARCHAR(500) COMMENT '用户原创作品图片URL',
`phone` VARCHAR(20) COMMENT '用户手机号(关联企业用户)',
`fail_reason` VARCHAR(500),
`webhook_event_id` VARCHAR(64) COMMENT '最近一次webhook事件ID幂等去重',
`synced_at` DATETIME COMMENT '最近同步时间',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_status` (`status`),
INDEX `idx_phone` (`phone`)
);
```
---
## 管理后台配置项
企业需在乐读派管理后台机构管理中配置以下 2
| 配置项 | 填写内容 | 说明 |
|--------|--------|------|
| Webhook URL | `https://你的域名/webhook/leai` | 接收作品状态变更推送第一步 |
| 认证回调URL | `https://你的域名/leai-auth` | H5 token 失效后跳回重新认证 |
---
> 完整接口规范、签名算法、代码示例请参考《AI绘本创作系统 企业后端集成指南 V4.0》及随附的 `enterprise-demo/java-demo/` 目录。