# 视频任务持久化与轮询重构

> 日期：2026-04-27
> 涉及模块：`yaoai-pipeline`、`yaoai-domain`、`yaoai-bootstrap`
> 关联文档：[08-异步任务与AI工作流](./08-异步任务与AI工作流.md)

## 1. 背景

线上反馈两类视频问题：

1. **历史视频链接过期 403**
   ```
   Code: AccessDenied
   Message: Request has expired
   ExpiresSeconds: 86400  // 24h
   ```
2. **复杂分镜偶发「视频生成超时（超过 10 分钟）」**，但 Ark 控制台查到任务实际仍在跑/已成功

两个问题共用一段代码：`VideoTaskAsyncProcessor.pollUntilDone`。

## 2. 根因分析

### 2.1 链接过期

`Seedance` 接口返回的 `videoUrl` 指向**火山 Ark 自家** TOS 桶（私有），它给我们的是预签名 URL，TTL 24h。
旧代码把这个临时 URL 直接持久化到 `ai_tasks.result_video_url`：

```java
// 旧实现
done.setResultVideoUrl(result.getVideoUrl());  // ← Ark 临时链接
```

24h 后用户访问就必然 `AccessDenied`。

### 2.2 误判超时

旧轮询实现：

```java
@Async
processTextToVideo(...) {
    submit to Ark
    pollUntilDone(taskId, externalId);   // ← 阻塞 10 分钟
}

private void pollUntilDone(...) {
    for (i = 0; i < 120; i++) {           // 120 × 5s = 10min
        Thread.sleep(5_000);
        query Ark;
        if (terminal) return;
    }
    mark as "视频生成超时（超过 10 分钟）";  // ← 硬判失败
}
```

问题：

- **占用 @Async 线程 10+ 分钟**，并发任务一多线程池就堵
- **服务重启会丢任务**：内存里的轮询循环消失，但 DB 里的状态仍是 `submitted`，永远没人继续处理
- **10 分钟阈值太紧**：复杂分镜（多参考图 + 配音 + 多镜头描述）经常 12–15 分钟才出，被误标失败

## 3. 新架构

### 3.1 流程对比

**改造前（in-thread polling）**

```
HTTP 请求 → @Async 线程
              ├─ 提交 Ark
              ├─ 阻塞循环查询 Ark （持续 10 分钟）
              ├─ 拿到 URL，原样写库 ❌（Ark 临时 URL）
              └─ 超时 → 硬标 failed ❌
```

**改造后（scheduled scanner + 转存）**

```
HTTP 请求 → @Async 线程（短命）
              ├─ 提交 Ark
              ├─ DB: status=submitted, external_task_id=xxx
              └─ 立即结束 ✅

@Scheduled (每 10s)：
  VideoTaskPoller
    ├─ 扫 ai_tasks WHERE status IN ('submitted','running')
    ├─ for each：
    │    ├─ 若 created_at 超 30min 仍非终态 → 标 failed（硬兜底）
    │    └─ 否则 → VideoTaskCompletionService.pollOnce(task)
    │
    └─ pollOnce()
         ├─ 查 Ark 状态
         ├─ succeeded → persistVideoToTos() → 存 publicUrl
         ├─ failed → 写 error_message
         └─ running → 同步状态

persistVideoToTos()：
  ├─ HTTP 下载 Ark 临时 URL（趁 24h 内）
  ├─ 上传到我们 public-read bucket
  │     key = {tenantId}/{projectId}/video/{taskId}.mp4
  └─ 返回 tosService.publicUrl(key)  ✅ 永不过期
```

### 3.2 组件职责

| 组件 | 职责 |
|---|---|
| `VideoTaskAsyncProcessor` | 提交 Ark + 写初始状态，**无轮询** |
| `VideoTaskPoller` | `@Scheduled` 调度器：扫描、超时兜底、批量分发 |
| `VideoTaskCompletionService` | 单条任务的查询/持久化逻辑（可被未来管理后台补救接口复用） |
| `AiTaskMapper.findPollableVideoTasks` | 拉所有 `submitted/running` 且有 externalTaskId 的任务 |

## 4. 关键改动文件

| 文件 | 类型 | 说明 |
|---|---|---|
| `yaoai-bootstrap/.../YaoAiApplication.java` | 改 | 加 `@EnableScheduling` |
| `yaoai-domain/.../AiTaskMapper.java` | 改 | 加 `findPollableVideoTasks(int limit)` |
| `yaoai-pipeline/.../async/VideoTaskAsyncProcessor.java` | 改（精简） | 删除 `pollUntilDone` + `persistVideoToTos`，提交后立即返回 |
| `yaoai-pipeline/.../async/VideoTaskCompletionService.java` | 新增 | `pollOnce(task)` + `persistVideoToTos(...)` |
| `yaoai-pipeline/.../async/VideoTaskPoller.java` | 新增 | `@Scheduled` 调度器 |

## 5. 配置项

| Key | 默认值 | 说明 |
|---|---|---|
| `yaoai.video.poll-interval-ms` | `10000` | `VideoTaskPoller` 轮询间隔 |
| `BATCH_LIMIT`（常量） | `50` | 单轮最多处理任务数，避免短时打爆 Ark |
| `HARD_TIMEOUT`（常量） | `30 min` | 任务硬超时阈值 |

> 常量后续可按需要外化到配置。

## 6. TOS Key 命名约定

```
{tenantId}/{projectId}/video/{taskId}.mp4
```

与 `TosService.buildKey` 的多租户隔离规范保持一致。

> 配套前提：bucket 已设为 `public-read`，且 `TosServiceImpl.upload` 调用时设了 `ACLType.ACL_PUBLIC_READ`。

## 7. 边界与已知限制

### 7.1 已存量过期 URL 救不回来
2026-04-27 之前完成的视频任务仍存的是 Ark 临时链接。Ark 那边的源文件可能已被清理（24h+），无法补救，需要让用户**重新生成**。

### 7.2 转存失败的兜底策略
`persistVideoToTos` 失败时（网络异常、TOS 写入失败），不抛错阻塞任务完成 —— 退而求其次写回 Ark 原始 URL（24h 内仍可用），打 ERROR 日志。

### 7.3 单实例轮询
当前 `@Scheduled` 在所有应用实例上都会执行。多实例部署时会有重复查询 Ark 的浪费（DB 写入是幂等的所以不会出错）。如未来扩到 K8s 多 Pod，应改用：
- ShedLock 之类的分布式锁
- 或者把扫描器单独放在 `yaoai-task` 模块只跑一份

### 7.4 Ark API 限流
单轮处理 50 条任务 × 一次 API 调用，10s 一轮 → 5 QPS。Ark 文档限额内（目前 60 RPM）。如果同时跑的视频任务超过 50，会有积压（下一轮再处理）。

## 8. 后续可选优化

| 项 | 优先级 | 说明 |
|---|---|---|
| 管理后台「重新拉取」补救接口 | 中 | 按 task id 重查 Ark；放在 `yaoai-admin` + `yaoai-admin-web` |
| `result_tos_key` 落库 | 低 | 当前只存 publicUrl 字符串；存 key 可解耦 bucket 迁移 |
| 多实例部署的分布式锁 | 低 | 见 7.3 |
| 视频生成耗时指标上报 | 低 | 用于评估 30 分钟阈值是否合理 |

## 9. 测试要点

- [ ] 提交一个新视频任务 → 5 分钟内完成 → 链接是 `aivideo-2026041417.tos-cn-beijing.volces.com/...` 而非 `tos-1az-front-azc...`
- [ ] 提交后立刻杀进程重启 → 任务能被 Poller picked up 并最终完成
- [ ] 模拟 Ark 长时间不返回 → 30 分钟后任务被标 failed
- [ ] Ark 返回 succeeded 但视频下载失败 → 任务仍标 succeeded，URL 是 Ark 原始 URL（兜底）
- [ ] 24h 后访问新生成视频的 URL → 仍可正常访问（验证 publicUrl 永久性）
