FSD M4 · 导出交付
← 总则 | 附录 → | 上游:PRD §4 M4 实现落点:
backend/internal/domain/export/(model/repo/service)+cmd/server/export_handlers.go+cmd/server/routes.go:82-92(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:434-471/routes2.js:318-439/routes3.js:347-376仅差异对拍参照。前端消费:ExportView.vue(编排)+ VideoView.vue(批量/下载面)。
M4 导出交付
10 端点全量。本域是创作主链的出口:导出任务(同步伪完成)、批量打标/渲染(跨域调 generation)、三种 ZIP 交付(素材包/成片包/剪映草稿)。蛇形 wire 域(ExportView 消费 shot_count 等蛇形键——与任务域驼峰 shotCount 相反,二十六期 A1 的镜像面);全域零 SSE(同步伪完成+前端 2s 轮询,service.go:4-5 注释)。计费两点:导出 2/段(type=export)、打标 10/镜写死(与 M3 tag 动态价刻意不同源)、渲染总额一次扣(VideoCost 公式)。zip 三端点二进制直出绕过 JSON 信封,错误分支仍走 JSON 信封。
F4.01 导出编排(export/task/:taskId / POST /export / export/:id)
对应 PRD:§4 M4 · GET /api/export/task/:taskId(①查重)、POST /api/export(②创建)、GET /api/export/:id(③轮询)| 状态:实现 | 实现落点:export_handlers.go:17-56、export/service.go:57-113、repo.go:70-146 | UI:
/export/:taskId(ExportView GET-or-POST+2s 轮询编排骨架——E1 整页豁免页)| 场景挂钩:E2E-EXP-01(待实现,清单 :19)
1 功能定义
导出任务三步编排:① 按任务查已有导出(防重复扣费);② 无则创建——按有视频分镜数扣 2/段、同步伪完成(status=complete/progress=100/无产物落盘);③ 轮询读单条。路由注记:task/:taskId 静态段与 :id 参数段并存,echo radix 静态子节点恒先于参数匹配(routes.go:83)。
2 触发条件与前置状态
- 导出页挂载 → ①;404 则 ②;此后每 2s → ③(前端 ExportView.vue:69-103:
api.get('/export/task/${taskId}')catch→api.post('/export',{task_id:Number(taskId)||taskId})→setInterval(poll,2000),终态done|complete|failed清定时器)。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| GET /export/task/:taskId | 路径 taskId | 任务非本人/不存在 → 404 not found;无导出记录 → 同 404 |
| POST /export | {"task_id":string}(蛇形逐字) | 任务属主查无 → 404 task not found(本端点专属文案,与全域 not found 不同字勿统一,model.go:113 注释冻结 index.js:456);余额不足 → 402 |
| GET /export/:id | 路径 id | 查无 → 404 not found |
② 的 _ = c.Bind(&req) 忽略解析错误——malformed JSON 时 req 零值续走业务(落 404 task not found);冻结 express.json 对 malformed 抛 400 SyntaxError(中央中间件)——DIF-29-23。
4 处理流程
- ① 查重响应:
ExportTaskView{ExportTask,…,title}(title 兜底空串——冻结.get(...)undefined→’’ 同)。 - ③ 轮询响应的 TaskTitle 查询无属主过滤照抄(repo.go:82-92 注记冻结 index.js:469 同——能命中的 export 行本就 user_id=?,无实际越权面)。
- file_path null 往返:DDL NOT NULL DEFAULT ‘’(P0 改造)→ Insert 落空串 → 读回 normalizeExportTask 空串→nil 还原响应 null 契约(repo.go:101-119——冻结 INSERT null 透传逐字保真)。
5 输出与结果状态
- ② →
{"id","status":"complete","shot_count":n}(三键,冻结 index.js:463 同)。 - ①③ → ExportTaskView:
id/user_id/task_id/status/progress/shot_count/file_path/created_at/title(file_path null 透传——API-增量.md:99);前端消费键title/shot_count/id/status/progress全蛇形。
6 状态流转
无状态机:status 唯一写入方恒 complete+progress:100(service.go:73-77);processing 仅是 DDL 默认/前端容错分支,实际不可达;无 update 路径、零 SSE——「伪完成」= 前端轮询首次即终态。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务不存在/非本人(②) | 404 | task not found(专属) |
| 任务不存在/无记录(①③) | 404 | not found |
| 余额不足 | 402 | 积分不足(零台账,无退款路径——CAS 失败即止) |
| 零分镜有视频 | 正常创建 shot_count=0 | — |
| malformed JSON(②) | 零值续走 → 404(DIF-29-23) | — |
8 权限与数据规则
- 权限:登录态+tasks 双键属主(
id=? AND user_id=?)。 - 数据规则:写
export_tasks/credit_records;ID 前缀e_(ids.go:22——冻结 uid(’e_’),P0 误写 ex_ 已 W5 勘误);export_tasks 含 shot_count 列(F3 补列修正——冻结 db.js 无该列而 INSERT 显式引用=冻结版该端点实际抛错,Go 按修后语义建列,PRD :103/:164 差异④)。
F4.02 一键串行打标(tasks/:id/export/tag-all)
对应 PRD:§4 M4 ④ · POST /api/tasks/:id/export/tag-all | 状态:实现 | 实现落点:export_handlers.go:57-66、export/service.go:120-148、repo.go:160-188 | UI:VideoView「一键打标」(confirm 文案
将对全部 N 个分镜串行打标,消耗 N*10 积分——前端重复实现价)| 场景挂钩:无
1 功能定义
对任务全部分镜的视频提示词串行追加一致性标签补丁;按 10/镜写死扣费(type=tag)。
2 触发条件与前置状态
- 导出准备区按钮(confirm 前置)→
POST /api/tasks/:id/export/tag-all(Bearer)。
3 输入与校验
路径 :id,无 body。TaskOwner 查无 → 404 not found。
4 处理流程
ListShotsAll(ORDER index_no)→ 先扣总额后逐镜 patch(cost := 10 * len(shots) 写死,service.go:129 注释「与单镜 Tag 的 F6 动态价不同源」;desc 一键串行打标签 ×N(-10N),type=tag)→ 逐镜 UPDATE shots SET video_prompt = CONCAT(COALESCE(video_prompt,''), ?),patch 逐字 `\n[一致性标签·自动] 主体一致 / 光影连续 / 运镜连贯 / <锚定上一镜|独立转场>`(MySQL 无 || 拼接→CONCAT 等价转换——API-增量.md:108 已登记)。
冻结 routes2.js:325 的 notifications INSERT 预备语句系死代码未调用,Go 不复刻(service.go:119 注释)。
5 输出与结果状态
{"tagged":len(shots),"cost":cost}。
6 状态流转
无状态机;shots.video_prompt 尾部追加(可重复点击重复追加——冻结同)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务非本人 | 404 | not found |
| 余额不足 | 402 | 积分不足 |
| 循环中途 UPDATE 失败 | 500(部分 patch 已落库无回滚——冻结同无事务) | internal server error |
8 权限与数据规则
- 权限:登录态+任务属主;写
shots/credit_records。
F4.03 批量极速渲染(tasks/:id/export/render-all)
对应 PRD:§4 M4 ⑤ · POST /api/tasks/:id/export/render-all | 状态:实现 | 实现落点:export_handlers.go:69-78、export/service.go:151-202、generation/service_video.go:539-569(RenderOne)| UI:VideoView「批量极速渲染」(confirm 估算文案含拉取+每秒价)| 场景挂钩:无
1 功能定义
对缺视频分镜批量提交渲染:总额一次扣(Σ VideoCost),逐镜串行调 generation.RenderOne;单镜失败退单价(退款唯一口在 generation 内),本层不重复退。
2 触发条件与前置状态
- 渲染按钮(confirm 前置)→
POST /api/tasks/:id/export/render-all(Bearer)。
3 输入与校验
路径 :id,无 body。TaskOwner 查无 → 404。空表不扣费单键响应 {"rendered":0}(service.go:162-166 冻结 :338 同)。
4 处理流程
- RenderOne 细节(generation 侧):secs 0→5;resolution 白名单外归
1080p(B5 收紧向已登记);prompt 裸传不加[resolution]后缀(与 M3 ⑩ generate-v2 的关键差异,service_video.go:540-541 注释);返回 status'processing'/'done'。 - 退款:RenderOne 真实通道提交失败 → SetVideoFailed+SetShotStatus(failed)+Refund(
ClaimVideoRefund认领防重,desc批量渲染失败自动退回(+N):<cause截80>)→ 502 被本层吞为 failed++(与冻结 catch 同)。
5 输出与结果状态
{"rendered","failed","cost","processing","note"} 五键恒在(全 Mock done 场景 processing=0、note 空串——service_test.go:396-397 场景锁)。
6 状态流转
videos 行 processing(真实异步)或 done(Mock);shots.status 随 Link 面联动(generation 域内)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务非本人 | 404 | not found |
| 无缺视频分镜 | 200 | {"rendered":0} |
| 余额不足 | 402 | 积分不足 |
| 单镜失败 | 退单价(generation 内)+ failed++ | — |
8 权限与数据规则
- 权限:登录态+任务属主;跨域经
BatchVideoGateway编译期断言接 generation(generation_assembly.go:118)。 - 数据规则:计价
video_pull + video_per_second×secs(secs<=0→5,默认 5/5 settings 可调);note 模板冻结 routes2.js:351 逐字。
F4.04 素材包交付(tasks/:id/export/assets-deliver)
对应 PRD:§4 M4 ⑥ · GET /api/tasks/:id/export/assets-deliver | 状态:实现 | 实现落点:export_handlers.go:81-90、export/service.go:204-257、repo.go:255-265 | UI:VideoView 下载素材包(前端自命名 qmtv-assets.zip,不读 Content-Disposition)| 场景挂钩:无(EXP-01 前置族)
1 功能定义
任务全部图片(storyboard/asset/anchor 三 kind+shot 关联图)打包 ZIP,无条件附 prompts.txt(全分镜图片/视频提示词清单)。零扣费。
2 触发条件与前置状态
- 下载按钮 →
GET /api/tasks/:id/export/assets-deliver(Bearer)。
3 输入与校验
路径 :id。TaskOwner 查无 → 404;图片行为空 → 400 没有可下载的图片。
4 处理流程
ListTaskImages((i.task_id=? AND i.kind IN ('storyboard','asset','anchor')) OR (s.task_id=?) LEFT JOIN shots ORDER index_no)→ 内存 zip 逐行:file_path 空/store.Read 失败静默跳过(冻结 existsSync=false 同);seen 去重在 Read 成功后置位(冻结同序);条目名 images/ + 前缀(IndexNo != nil && != 0 → shot%03d_ 否则 asset_——W6 期修对齐冻结 JS truthy 语义,0 与 null 同落 asset_;PRD 差异②引用)+ safeSlice(id,4,12)(img_ 前缀后 8 字符)+ 扩展名。prompts.txt:每镜 # 分镜%d 【%s】\n[图片提示词]\n%s\n\n[视频提示词]\n%s\n 以 \n join。Deflate 压缩。
5 输出与结果状态
二进制 ZIP 直出:Content-Type: application/zip + Content-Disposition: attachment; filename="qmtv-assets-<taskID前8>.zip"(前端不读该头自命名)。全部图片缺失仍 200(zip 只剩 prompts.txt)。
6 状态流转
无(纯打包)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务非本人 | 404 | not found |
| 无图片行 | 400 | 没有可下载的图片 |
| 个别文件缺失 | 静默跳过该条目 | — |
8 权限与数据规则
- 权限:登录态+任务属主;只读 images/shots。
F4.05 成片包交付(tasks/:id/export/deliver)
对应 PRD:§4 M4 ⑦ · GET /api/tasks/:id/export/deliver | 状态:实现 | 实现落点:export_handlers.go:93-102、export/service.go:259-293、repo.go:221-230 | UI:VideoView 下载成片(qmtv-videos.zip)| 场景挂钩:E2E-EXP-01(ZIP 魔数断言目标面,待实现)
1 功能定义
任务全部 status='done' 视频打包 ZIP,条目名含场景标题清洗。零扣费。
2 触发条件与前置状态
- 下载按钮 →
GET /api/tasks/:id/export/deliver(Bearer)。
3 输入与校验
路径 :id。TaskOwner 查无 → 404;无 done 视频 → 400 没有已完成的视频。
4 处理流程
ListDoneVideos(v.task_id=? AND v.status='done' LEFT JOIN shots ORDER index_no)→ zip 逐行:file_path 空行跳过(冻结 path.basename('') → readFileSync(FILES_DIR) 目录崩溃 500——Go 跳过不复刻崩溃,service.go:259-260 注释显式登记);条目名 shot%03d_<scene_title 清洗><ext>:IndexNo 0 兜底同冻结 v.index_no || 0;cleanFilename:[\\/:*?"<>|] → _ 且截 20(Go 按 rune、JS 按 UTF-16 单元——中文 BMP 等价)。
5 输出与结果状态
ZIP 直出,filename="qmtv-videos-<id8>.zip"。全部文件缺失 → 空 zip 200(冻结同语义)。
6 状态流转
无。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务非本人 | 404 | not found |
| 无 done 视频 | 400 | 没有已完成的视频 |
| file_path 空行 | 跳过(冻结崩溃 500 不复刻——DIF-29-24) | — |
8 权限与数据规则
- 权限:登录态+任务属主;只读 videos/shots。
F4.06 单镜参数(tasks/:id/export/scenes/:sid)
对应 PRD:§4 M4 ⑧ · PUT /api/tasks/:id/export/scenes/:sid | 状态:实现 | 实现落点:export_handlers.go:105-120、export/service.go:295-327、repo.go:192-219 | UI:VideoView 单镜参数表单(双 PUT 串行:本端点成功才发
/shots2/:id/params分辨率)| 场景挂钩:无
1 功能定义
单镜 duration/ratio 参数更新:duration clamp [1,15]、ratio 白名单外归 16:9。零扣费。
2 触发条件与前置状态
- 单镜参数保存 →
PUT /api/tasks/:id/export/scenes/:sid(Bearer)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| duration | JSON body | 否(*int) | 键缺失或 null 均跳过(对齐冻结 duration != null);clamp <1→1、>15→15 |
| ratio | JSON body | 否(*string) | 非 nil 且非空串才处理;白名单 9:16/16:9/1:1,白名单外归 ‘16:9’(冻结 routes2.js:403 同) |
三键属主存在性:shots JOIN tasks WHERE s.id=? AND t.id=? AND t.user_id=?,!ok → 404 not found。
4 处理流程
逐键独立 UPDATE(duration 一条、ratio 一条)。
5 输出与结果状态
{"ok":true,"data":true}。
6 状态流转
shots.duration/ratio 列更新;无其他联动。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 镜/任务不匹配或非本人 | 404 | not found |
| duration=0/20 | clamp 1/15 | — |
| ratio=4:3 | 归 16:9 200 | — |
8 权限与数据规则
- 权限:登录态+三键属主链(shot+task+user)。
F4.07 导出执行记录(tasks/:id/export/history)
对应 PRD:§4 M4 ⑨ · GET /api/tasks/:id/export/history | 状态:实现 | 实现落点:export_handlers.go:123-132、export/service.go:329-349、repo.go:232-253 | UI:VideoView 导出记录区(useQuery
['export-history', taskId])| 场景挂钩:无
1 功能定义
任务关联全部视频的执行记录读(无 status 过滤),每行注入模型名与参考图计数。零扣费。
2 触发条件与前置状态
- 记录区加载 →
GET /api/tasks/:id/export/history(Bearer)。
3 输入与校验
路径 :id。TaskOwner 查无 → 404 not found。
4 处理流程
ListHistoryVideos(v.*, s.index_no, s.scene_title LEFT JOIN shots ORDER v.created_at DESC——无 status 过滤、无次级排序键)→ 每行注入 model = billing.View().VideoModel(冻结 getBilling().models.video 同)→ 每行 ref_images = COUNT(shot_images WHERE shot_id=? AND image_id IS NOT NULL)(Go ShotID 空串计 0——冻结 v.shot_id || '' 同语义;每行一次 N+1 查询,冻结同构保真)。
5 输出与结果状态
VideoHistoryRow 数组,行键 18 个:id,user_id,task_id,shot_id,standalone,prompt,status,file_path,error,attempts,seconds,resolution,refunded,created_at,index_no,scene_title,model,ref_images(version 不出 wire)。index_no/scene_title LEFT JOIN 可空→指针 null。
6 状态流转
无(纯读)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务非本人 | 404 | not found |
| 无视频 | 200 空数组 | — |
8 权限与数据规则
- 权限:登录态+任务属主。
- 数据规则:已登记差异①——响应
v.*列序与冻结(db.js CREATE 后 ALTER 折叠序)不齐:键集一致、前端按键取值无感,严格逐字节对拍不齐(PRD :164;ExportTaskView 字段序同族)。
F4.08 剪映草稿(tasks/:id/jianying-draft)
对应 PRD:§4 M4 ⑩ · GET /api/tasks/:id/jianying-draft | 状态:实现 | 实现落点:export_handlers.go:135-144(zipRespond :148-151)、export/service.go:353-457 | UI:VideoView 下载剪映草稿(qmtv-jianying.zip)| 场景挂钩:无
1 功能定义
按 done 视频序列生成剪映草稿演示包:draft_content.json(μs 时间轴)+ 素材/ + 使用说明.txt 三件套 ZIP。零扣费。
2 触发条件与前置状态
- 下载按钮 →
GET /api/tasks/:id/jianying-draft(Bearer)。
3 输入与校验
路径 :id。TaskOwner 查无 → 404;无 done 视频 → 400 没有已完成的视频。
4 处理流程
ListDoneVideos(同 F4.05 SQL)→ folder=QMTV剪映草稿_<title清洗截20> → draft 键序逐字 {name, duration, materials:{videos:[{id,path,duration}]}, tracks:[{type,segments:[{material_id,start,duration}]}]},初始 tracks:[{type:"video"}] → 逐视频:secs=seconds 0→5、us=secs×1_000_000、fname=shot%03d.svg(IndexNo==0 → i+1 兜底;视频文件 .svg 后缀怪形态逐字照抄,service.go:380-381 注释冻结 routes3.js:363 同)→ materials push {id:"mat_<i>", path:"./素材/<fname>", duration:us}、segments push {material_id:"mat_<i>", start:i×us, duration:us} → draft 引用与 zip 收录解耦(draft push 无条件;zip 仅 store.Read 成功且 file_path≠"" 收录——冻结 routes3.js:362-368 同)→ draft_content.json MarshalIndent 缩进 2 → 使用说明.txt 五步说明+「正式版将生成完整剪映工程格式」提示逐字。
5 输出与结果状态
ZIP 直出,filename="qmtv-jianying-<id8>.zip":
6 状态流转
无(纯打包)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务非本人 | 404 | not found |
| 无 done 视频 | 400 | 没有已完成的视频 |
| 素材文件缺失 | draft 仍引用、zip 缺该条目(解耦保真) | — |
8 权限与数据规则
- 权限:登录态+任务属主。
- 数据规则:draft
duration由循环累加(冻结 reduce 计算——等价);mat_<i>物料 id 从 0 起。
M4 端点覆盖对账
| # | 端点 | 功能节 |
|---|---|---|
| 1 | POST /api/export | F4.01 |
| 2 | GET /api/export/task/:taskId | F4.01 |
| 3 | GET /api/export/:id | F4.01 |
| 4 | POST /api/tasks/:id/export/tag-all | F4.02 |
| 5 | POST /api/tasks/:id/export/render-all | F4.03 |
| 6 | GET /api/tasks/:id/export/assets-deliver | F4.04 |
| 7 | GET /api/tasks/:id/export/deliver | F4.05 |
| 8 | PUT /api/tasks/:id/export/scenes/:sid | F4.06 |
| 9 | GET /api/tasks/:id/export/history | F4.07 |
| 10 | GET /api/tasks/:id/jianying-draft | F4.08 |
10/10 对账 routes.go:82-92 与 PRD §4 M4。已登记差异引用:PRD 差异①(history 列序)②(index_no=0 truthy——W6 修后形态)③(repo 16 方法计数)④(shot_count 列 F3 补列修正);API-增量.md:108(CONCAT 方言/video_id DDL 谓词/file_path null 往返)三条。
注记(非 DIF):① routes.go:80 注记引用「PRD :136」行号漂移(M4 清单实际在 :162)——文档面,留勘误;② E2E-EXP-01 长期「待实现」且冒烟列=是(清单 :19),三十一期反向覆盖收口候选;③ ⑤ RenderOne 单镜重算 cost 与响应回传调用方总额之间存在 settings 中途变更窗口(单请求内,实践不可达)。
DIF 新发现(本期取证,入 99-附录 B):DIF-29-23 export ②⑧ bind 错误静默忽略(零值续走业务;冻结 malformed JSON 400 SyntaxError 中央中间件)/DIF-29-24 deliver 空 file_path 行跳过 200(冻结 readFileSync 目录崩溃 500——service.go:259-260 代码注释已登记、PRD §4 M4 正文未含)。