通过 VITE_AUTO_FILL_TEST 环境变量控制,在 .env.test 中启用, 使测试环境构建后登录框也能自动填充测试账号,方便测试人员使用。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14 KiB
14 KiB
企业同步创作数据 — 核心三步
基于《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 伪代码)
@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)
@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 调用签名示例
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 同时触发)
推荐两种方案防止同一作品被并发更新:
-- 方案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)
/**
* 用户点击作品详情页时调用
*/
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 做路由跳转:
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(主动请求) |
企业推荐表结构
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/目录。