跳转到主要内容

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 边界与异常
条件系统行为提示/码
任务不存在/非本人(②)404task not found(专属)
任务不存在/无记录(①③)404not 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 边界与异常
条件系统行为提示/码
任务非本人404not 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 边界与异常
条件系统行为提示/码
任务非本人404not 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 边界与异常
条件系统行为提示/码
任务非本人404not 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 边界与异常
条件系统行为提示/码
任务非本人404not 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 输入与校验
字段来源必填校验
durationJSON body否(*int)键缺失或 null 均跳过(对齐冻结 duration != null);clamp <1→1、>15→15
ratioJSON 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 边界与异常
条件系统行为提示/码
镜/任务不匹配或非本人404not found
duration=0/20clamp 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 边界与异常
条件系统行为提示/码
任务非本人404not 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":

qmtv-jianying-<taskID前8>.zip
└── QMTV剪映草稿_<title清洗截20>/
    ├── 素材/shot001.svg…        # 视频字节、.svg 后缀怪形态
    ├── draft_content.json       # 缩进 2;duration=Σμs
    └── 使用说明.txt
6 状态流转

无(纯打包)。

7 边界与异常
条件系统行为提示/码
任务非本人404not found
无 done 视频400没有已完成的视频
素材文件缺失draft 仍引用、zip 缺该条目(解耦保真)—
8 权限与数据规则
  • 权限:登录态+任务属主。
  • 数据规则:draft duration 由循环累加(冻结 reduce 计算——等价);mat_<i> 物料 id 从 0 起。

M4 端点覆盖对账

#端点功能节
1POST /api/exportF4.01
2GET /api/export/task/:taskIdF4.01
3GET /api/export/:idF4.01
4POST /api/tasks/:id/export/tag-allF4.02
5POST /api/tasks/:id/export/render-allF4.03
6GET /api/tasks/:id/export/assets-deliverF4.04
7GET /api/tasks/:id/export/deliverF4.05
8PUT /api/tasks/:id/export/scenes/:sidF4.06
9GET /api/tasks/:id/export/historyF4.07
10GET /api/tasks/:id/jianying-draftF4.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 正文未含)。