# 企业同步创作数据 — 核心三步

> 基于《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);

    // 本地状态 < 3（PENDING 或 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/` 目录。
