# 企业同步创作数据 — 核心三步 > 基于《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 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 params = new LinkedHashMap<>(); params.put("orgId", ORG_ID); params.put("updatedAfter", "2026-04-05T00:00:00"); // 生成 HMAC 签名头(4个 Header) Map 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/` 目录。