这是本节的多页打印视图。 .
QMTV 产品文档
- 1: 产品
- 1.1: 00 · 交接导览(先读我)
- 1.2: QMTV 移植契约规格(PRD)v1
- 1.3: 01 · 系统设计方案
- 1.4: 前端技术对比:qmtv(Vue3)vs pms web(React18+AntD5)
- 1.5: 04 · 待开发功能与规划
- 2: 使用手册
- 2.1: QMTV 使用说明书(页面图解版)
- 2.2: 人工测试清单(QMTV QA 走查底稿)
- 2.3: QMTV 使用手册 · 文档索引
- 3: 测试
- 3.1: QMTV 测试规划(Go 版,P0 底册)
- 3.2: QMTV E2E 测试约定
- 3.3: QMTV E2E 场景清单(覆盖地图)
- 4: 功能规格(FSD)
- 4.1: FSD M1 · 认证与用户
- 4.2: FSD M10 · 导演与融合
- 4.3: FSD M11 · 评估与工具
- 4.4: FSD M12 · 管理后台
- 4.5: FSD M13 · 火花计划
- 4.6: FSD M2 · 任务与拆解
- 4.7: FSD M3 · 生成与素材
- 4.8: FSD M4 · 导出交付
- 4.9: FSD M5 · 通知公告
- 4.10: FSD M6 · 组织协同
- 4.11: FSD M7 · 分销提现
- 4.12: FSD M8 · 充值
- 4.13: FSD M9 · 剧本库与投稿
- 4.14: FSD 附录 · PRD 对照 / 代码域索引 / 差异台账
- 4.15: QMTV 功能规格说明书(FSD)v1.0 · 总则
1 - 产品
- 项目导览——第一页:QMTV 是什么、能做什么,速览与文档地图
- PRD(v1)——生产工具+内容社区+商业化三合一的产品需求:用户旅程、积分体系与模块清单
- 系统设计方案(01)——三合一定位、积分体系、AI 链路与核心模块设计
- 前端技术对比(v1)——前端技术选型评估与结论
- 待开发功能与规划(04)——功能补强与演进规划
1.1 - 00 · 交接导览(先读我)
本目录是 QMTV 项目开发文档集,面向开发/运维的同事。建议按顺序阅读,全程约 30 分钟即可建立完整认知并跑起项目。
项目速览卡
| 项 | 值 |
|---|---|
| 项目名 | QMTV — AI 短剧智能生产线 |
| 位置 | D:\WORK\GAE\qmtv(本目录) |
| 技术栈 | Node.js 22 + Express 5 + better-sqlite3(后端);Vue 3 + Vue Router 4 + Vite 8(前端);无 UI 框架,自研主题 |
| 启动 | npm install → npm start(后端 :3300 并托管前端 dist)→ npm run build(构建前端) |
| 数据库 | data/qmtv.db(SQLite WAL,49 张表,免安装) |
| 规模 | 后端 24 个模块 / 5436 行,239 个 API;前端 24 个页面 / 约 9000 行 |
| 设计文档(历史全量) | D:\WORK\GAE\QMTV网站设计文档.md(22 节,按 v1→v22 版本演进记录每一步设计决策与实测结论) |
文档地图
| 文档 | 内容 | 什么时候读 |
|---|---|---|
| 01-系统设计方案.md | 产品定位、功能全景、各模块设计方案、数据模型、计费模型、AI 链路设计、视觉体系 | 想理解"为什么这样设计" |
| 02-系统架构介绍.md | 技术栈、目录结构、后端模块职责、前端结构、49 张表清单、关键机制(SSE/供应商适配/启动恢复)、构建部署 | 想理解"代码怎么组织的" |
| 03-系统开发说明.md | 环境准备、启动步骤、账号、日常开发操作指南(加页面/加接口/加表)、验证方法、踩坑清单、运维注意 | 动手开发前必读 |
| 04-待开发功能与规划.md | 真实模型接入清单、已知欠账与改进项、建议优先级 | 排后续开发计划 |
| 05-API接口速查手册.md | 全部 239 个 API 按模块分组 | 写代码时当字典查 |
30 分钟上手路径
- (5 分钟)读本文档 +
02-系统架构介绍.md的目录结构一节 - (5 分钟)
npm install,两个终端分别npm start和npm run dev(前端热更 :5173,代理到 :3300) - (5 分钟)用制片账号登录
producer@xingmang.cn / demo123456,把主导航每一页点一遍;再用管理员admin@qmtv.com / admin123456看管理后台 - (10 分钟)读
03-系统交接说明.md的「常见开发任务怎么做」与「踩坑清单」 - (5 分钟)浏览
04-待开发功能与规划.md,确定你的第一个任务
三条最重要的经验(先记住再说)
- 发中文请求调试一律写 Node 脚本(仓库里有大量
verify_*.js可参照),不要用 curl/heredoc——Git Bash 下中文会 GBK 损坏,曾数次被误判为 bug。 - 数据库是SQLite 单文件,改表结构统一走
server/db.js的 migrations 数组(ALTER TABLE ... ADD COLUMN,try/catch 幂等),不要手改已有 CREATE TABLE。 - 所有生成动作都走积分:先
tryDeduct再调providers.imageSmart/videoSmart,失败退款由 providers 内部统一负责,调用方不要重复退款。生成类入口只此一个约定,接手时务必遵守。
1.2 - QMTV 移植契约规格(PRD)v1
文档定位:本文是 qmtv 后端 Go+MySQL 移植的需求契约唯一权威——以冻结版 Node 实现(
qmtv/server/,2026-10-17)的实际行为为基准逐条登记,外加已裁决的修正项。Go 版若与本文不符,以本文为准;本文与冻结代码不符处=修正项章节。上游设计参照:qmtv/docs/01-系统设计方案.md(冻结)。后续 FSD 以 Go 实现代码为事实基准反向对账本文;本文修订走版本记录(§0.1)。
0.1 修订记录
| 版本 | 日期 | 修订说明 | 作者 |
|---|---|---|---|
| v1 | 2026-10-17 | 初版:239 端点全清单 + 6 修正项 + 全局契约 | Claude |
| v1.1 | 2026-10-17 | 前端项目立项:新建 frontend/(初始复制 qmtv 前端,行为对等),演进轨道 vue-query/vitest/TS(§1 前端行更新 + 新增 §7);C3/C4/§2.1 token query 表述按冻结实际行为修正(全局兜底,非白名单) | Claude |
| v1.2 | 2026-10-18 | P1 期首核查勘误 + 裁决落册:C4/§2.1 修正为冻结三分事实(v1.1 的「全局兜底、与冻结一致」表述被核查证伪——冻结 authRequired 仅 Bearer/events 纯 query/images/:id query 优先)+ 登记 Go 裁决(FromRequest 保持 Bearer 优先 + query 全局兜底);F1 兼容影响改写(前端 api.js:24 补丁须同期);F2 谓词微调;新增 F7(邮箱校验统一)/F8(素材对象存储);M1 端点数 6→9(补登 logout/credit-records);§2.2 rr_ 共用注记 | Claude |
| v1.3 | 2026-10-18 | P2 立项定稿:F2 尾注更新(RunWithRetry 实测不引入——CAS 谓词仅 credits>=? 下 affected=0 ⇔ 余额不足,qmtv/pms 双侧零生产调用);F8 补 file_path 布局差异(ObjectKey 日期子目录 vs 冻结扁平);新增 F9(退款防重唯一口,修冻结双退款漏洞);§6 启动恢复注记 P2 范围拆分(workbench 随 M11);跨聚合扣款时序定稿(§6,不引 actor/outbox 对齐冻结同步流) | Claude |
| v1.3-pending | 2026-10-18 | W10 对账预告(pending,正文暂不改):M2 端点数实测 38(routes.go M2 组实装口径),§3 表 :33/§4 :118/合计 :45 的「66」系 P0 统计残留——W10 收尾时三处一并校准;增量端点逐条清单移 docs/API-增量.md 累加 | Claude |
| v1.3-pending | 2026-10-18 | C1 裁决落地(storage 默认面):dev 模板默认直连 MinIO(192.168.50.190:9000+bucket qmtv+public_base,凭据 LAN 实例默认不入生产);F8 增记:MinIO 模式落库 file_path=终态绝对 URL(pathOfFunc 写路径拼 public_base,前端逐字消费;桶/域迁移需一次性 UPDATE file_path),local 模式仍 /files/ 相对(=冻结形态)——F8 原文「落库相对+出站现场拼」按此修订 | Claude |
| v1.3-pending | 2026-10-18 | W10 对账预告(M3,pending,正文暂不改):M3 端点数实测 19(index.js 9+routes2 3+routes3 7),§3 表/§4 标题的「38」系 P0 统计残留——render-all 实属 M4、retry 无用户端点;/api/models/active 归属补登 M3(PRD 全文未列该端点,W10 一并补登) | Claude |
| v1.3-pending | 2026-10-18 | M3 实现差异登记(W4 期首核查 B5/B6 补录,W10 入正文):①expires_at=C7 例外全达成(冻结 toISOString() 入库透传;Go ISOTime 响应 ISO,落库仍 DATETIME(3))②SmartVideo ⑩ 分辨率白名单外值(如 480p)冻结原样透传 vs Go 归一 1080p、ratio null 冻结传 null vs Go 归一 16:9(Go 收紧向,安全)③SSE progress 冻结每次 fetch 前发 vs Go fetch 后发(tries 命中集合相同)④tag 打标价冻结固定 COSTS.image=10 vs Go 随 F6 settings 覆盖价 | Claude |
| v1.3-pending | 2026-10-18 | M4 实现差异登记(W5 期首核查补录,W10 入正文):①history 响应 v.* 列序:冻结 created_at 在 error 之后(db.js CREATE 后 ALTER 折叠序)vs Go DDL/struct 排末位——键集一致、前端按键取值无感,严格逐字节对拍不齐;②assets-deliver 条目名 index_no=0:冻结 JS truthy→asset_ vs Go 原指针非 nil→shot000_,W6 期已修对齐(!= nil && !=0);③export repo 接口实为 16 方法(基线「17」系计数漂移) | Claude |
| v1.3-pending | 2026-10-18 | W6/W8 实现差异登记(期首核查 B2 + W8 集成抽样补录,W10 入正文):①W6 逐段容错差异——冻结 videos 段无 catch(异常即崩启动,SQLite 同步 API 副作用非设计)vs Go 统一逐段容错不挡启动;②workbench finished_at 冻结 datetime('now','localtime') vs Go NOW(3)(方言惯例);③§6 :196「workbench_tasks 扫描随 M11 落地」已过时——W6 已内联实现(正文 W10 校准一并改);④W8 集成抽样抓获:task 恢复扫描 SELECT * 遇可空列 NULL 整段静默 no-op(Task 宽结构体 5 个非指针可空列),已修窄投影 SplitRecoverRow(语义等价);同族脆弱仍在 task.Task/Shot 宽扫描面(生产 InsertTask 全列写不可达,人工/迁移行可达)——指针化 vs 维持全列写纪律待裁决;⑤seed Go 版一步到位差异:producer@xingmang.cn 独立账号(referral_code=QMXINGMANG01,与 demo 并存)+ 台账单笔 init(沿 SQL seed 惯例;冻结 demo 为 0 元 register 形态) | Claude |
| v1.3-pending | 2026-10-18 | A1 落地(W8 十续期首核查,行④同族首例生产可达已裁决根治):seed.go 部分列 INSERT 沿冻结 seed.js:46 形态致 producer 示例任务 5 可空列落 NULL——ListByUser/ByID 宽扫描 500(集成实测红坐实);用户裁决 DDL 收紧:delta/0002 tasks 5 列+shots 2 列 UPDATE 回填+MODIFY NOT NULL(MySQL TEXT 无字面 DEFAULT——漏写列 strict mode 显式红=守卫语义)+seed INSERT 补列+full 重拼;C7 例外不动:videos.prompt/error 保持 *string NULL 透传(冻结语义);director_shots(M10)/workbench_tasks(M11)待域落地随域裁决;行④「生产不可达」陈述由本行取代;同票:prompt_templates 补种 5 行(对齐冻结 ensureSeed 存储面,Go prompts 内置不接线产物无差) | Claude |
| v1.4 | 2026-10-18 | W10 对账落地(十一续期首核查后收尾,pending 行 :15-:21 内容全部入正文,行本身保留不改写):①端点数校准——M2 66→38、M3 38→19(routes.go 实装亲数口径,storyboard/generate 归 M2 组、models/active 补登 M3、M4 勘误「GET /api/export」实为 GET /api/export/:id 轮询面),合计 242→239(模块数直加恰合 v1/CLAUDE.md 实测冻结 239——原表直加 286 从未自洽),§3 表新增「Go 已实现」列(基础 3+M1 | Claude |
| v1.5 | 2026-10-18 | M9 落地 + uid 前缀全量复核(十三续):§3 表 M9 行 Go 已实现 25(合计 79→104);§2.2 uid 前缀表全量复核回填(冻结 47 前缀亲数——P0 误写 8 处勘误:lib_→sl_/nar_→na_/dp_→dpr_/rch_→ro_/rc_→cm_/wd_→w_/af_→fd_/w_→wk_ 错位,补缺 7 个 an_/ex_/ih_/inv_/me_/oa_/om_,ids_test.go 全量对拍守卫钉死);M9 实现差异七项入 §4 M9 节(upload 修复语义等);narrations type=‘video’ 扣 30 走 F9 同源 TryDeduct(taskId 空串=冻结 null) | Claude |
| v1.6 | 2026-10-18 | 十四续期首核查回写(A2B7C2→修复落地):§2.2 补登 pt_(B1,原 46/47);§4 M9 差异增 ⑧writers search TrimSpace 保持+登记(B5 用户裁决)/⑨narrations 台账 task_id '' vs null(B7)——二十一期⑤收口(0003 列 NULL 化):credits 域 INSERT 绑定 ''→NULL 单点收口,无关联台账行恒 NULL 对齐冻结 `taskId | |
| v1.7 | 2026-10-19 | 十五续(M12 管理后台落地 + 期首核查勘误):①期首核查 2026101823(A0B2C0)勘正——API-增量「admin 8」→9 两处(B-新1)、NEXT-SESSION「25 mutation」→24 口径(B-新2,归档段不回改);②M12 亲数勘误 36→39(41 物理 admin 路由 − withdrawals 2 归 M7;instruction-versions 通配展开+DELETE 漏登补齐),§3 合计 239→242(v1.3 校准时 M12 残留未清;v1 原合计 242 系巧合重合),根 CLAUDE.md 规模基线行同步;③M12 域落地 internal/domain/admin 39 端点(A 用户积分 8/B 生成审计 3/C 公告 3/D 组织审批 3/F 计费配置 4/G 模型接入 6/H 提示词中心 12;死端点 model-settings×2+instruction-versions/active 照抄;ErrNoUser 吞形保冻结信封;api_key 明文回显照抄+安全注记;instruction-versions DELETE 按修后语义打 prompt_templates——冻结查死表 instruction_versions 恒 404,F3 同制登记);④存量真 bug 修复:modelCfg.setting() key 未反引号恒 ERROR 1064(P0 起读侧模型配置链 LLMConfig/endpointByPurpose/genBilling.models 全哑,Mock 优先架构掩盖)——集成链「读侧即时生效」断言首证抓获,反引号一处修复;⑤跨域共表写归主域:task.AdminRetrySplit(retry-split 不删根资产=冻结原样)/generation.ClaimManualRelease(CAS+CONCAT(COALESCE) 一步);E1 面零改动,migrations 53 不变,delta 无 0003 | Claude |
| v1.8 | 2026-10-19 | 十六续(期首核查 A1B2C0 修复 + M7 分销提现落地 + 亲数勘误):①期首核查 2026101900(十五续 bc216dd)——A1 ResetPassword 校验顺序与冻结相反(Go 先判长度后查存在 vs 冻结 index.js:140-142 先查存在),用户裁决修序对齐(admin/service.go + service_test 用例先红后绿);B1 §4 M12 补登记 ⑨providers/publish 反向死调用不实现=echo 404 兜底(上期计划承诺「PRD 登记一句」漏落);B2 根 CLAUDE.md 勘正日期微疵;②M7 亲数勘误 12→11(冻结 index.js :598-:638/:708-:737 亲数恰 11、§4 清单自列即 11——P0 统计残留第四例),§3 合计 242→241(上期 239→242 勘误时 M7 残留未清,亲数为准),M7 冻结源文件列勘正(11 条全在 index.js,recharge.js 仅承载返佣写入属 M8 链)、API-增量 合计节与根 CLAUDE.md 基线行同步;③M7 域落地 internal/domain/distribution 11 端点全量(integration 第七链 DistributionChain;零 credits 依赖/零 DDL/零 SSE;W_FLOW 严格邻接禁跳转;available「cancelled/rejected 不释放」冻结谎言照抄+集成双断言钉死;settings 双回退 shape/980 硬编码/exchanges 死态 quirks 逐字;差异 ①~⑩ 入 §4 M7 节);AdminView 提现 tab 404 面转绿(前端零改动);migrations 53 不变,delta 无 0003 | Claude |
| v1.9 | 2026-10-19 | 十七续(期首核查 A0B2C0 回写 + M8 充值返佣落地 + 亲数勘误):①期首核查 2026101903(十六续 c2b2a29)——A 类 0 项;B1「service_test 13 场景」实为 10 测试函数(断言点无缺失,计数漂移,本段起以 10 为权威口径);B2 integCleanup 未扩 distribution 四表(M8 链落地时收口);②M8 亲数勘误 5→8(recharge.js 亲数恰 8、§4 清单自列即 8——P0 统计残留第五例,§4 清单一直正确而标题/§3 行少算,与 M7 同型反向),§3 合计 241→244(十六续 242→241 勘误时 M8 残留未清,亲数为准),根 CLAUDE.md 基线行同步;③M8 域落地 internal/domain/recharge 8 端点全量(integration 第八链 RechargeChain:config 对拍/下单 rate 换算/凭证真 multipart 落盘/approve 到账对账+两级返佣 8·2/资格联动 ≥980+已开不重复/reject 双截断/admin list 过滤/403/404;commission_records 唯一写入方收口=M7 差异①;approveOrder 冻结死导出不建恢复链;差异 ①~⑧ 入 §4 M8 节);integCleanup 扩 distribution 四表+recharge_orders(B2 收口);RechargeView/AdminView 充值面 404 转绿(前端零改动);migrations 53 不变,delta 无 0003 | Claude |
| v1.10 | 2026-10-19 | 十八续(期首核查 A1B2C0 修复 + M6 组织协同落地 + 亲数勘误):①期首核查 2026101907(十七续 f27cd9a)——A1 recharge service_test 宣称 12 函数实为 11+三处承诺断言面缺失(rel2 缺失正向对拍/SSE payload/round2 小数域),补强至 12 函数(恰合宣称口径,历史段无需回改)+负样本演练两发先红后绿;B1 API-增量 头注「241」漏勘(本期随 235 一并收口);B2 归档计划「85」旧口径(append-only 登记权威口径,十七续段 82 为准);②M6 亲数勘误 38→29(冻结 index.js 组织段 9+enterprise.js 20=29、§4 清单自列即 29——P0 统计残留第六例,与 M7 同型「标题多算清单对」),§3 合计 244→235,根 CLAUDE.md 基线行「四轮亲数勘误」;③M6 域落地 internal/domain/org 单包多文件七件 29 端点全量(四零域:零 admin/零上传/零扣费/零 SSE 零通知;join 三写禁事务直译;权限分层 owner 严格 vs adminOrOwner 唯一面;normalizeMaterialPerms 联动纯函数;invite code crypto/rand 8 位大写 base36;integration 第九链 OrgChain:approve 建组织→invite/join→角色三哨兵→dashboard available==balance 钉死→material-perms 联动→showcase 恒 ok→share/search/401;integCleanup 扩组织八表+users.org_id 复位先于删 users,孤儿终验五零;执行期抓获 service percent 公式错(×1000 应 ×10——测试先红抓获即修);gorp 嵌套 struct 平铺教训两发〔Org 平铺/OrgMemRef 补 db tag〕);OrganizationView 等五视图 404 面转绿(前端零改动);差异 ①~⑩ 入 §4 M6 节;migrations 53 不变,delta 无 0003 | Claude |
| v1.11 | 2026-10-19 | 十九续(期首核查 A3B4C1 修复 + C1 补投影 + M10 导演与融合落地):①期首核查 2026101908(十八续 9cab5dd)——A1 projectRank wire 键 json:"amount-kind" 勘误 kind(前端 OrganizationView「类型」列恒空白;wire 键集断言守卫自此入惯例:结构体字段断言不走 json tag);A2「孤儿终验五零」宣称无载体——TestIntegration_IntegCleanupLeavesNoOrphans 守卫落地(负样本注释清理 SQL 先红坐实);A3 perms_test 补非空无 view 归空正向用例+用例名勘误;B1 测试计数宣称漂移两处(service_test 实 20 函数/perms_test 实 2 函数 14 用例)本段为权威口径;B4 冻结底册 05 手册头注 239 旧口径+/api/api/ 双前缀 9 处记遗留(冻结区不回写);②C1 用户裁决补齐 shared-projects script_text 投影(冻结 dp.* 全列)+A4 勘正十八续误加谓词(projectRank task_id != ''/shared-projects video_id != ''——冻结 IS NOT NULL 对空串放行,「独立生成 / 工具」聚合行曾被整行挡掉;MySQL NOT NULL 列上 IS NOT NULL 即冻结字面);M6 差异 ⑪ 入 §4(assign 响应 null vs “");③M10 亲数复核 28 清点清白(director.js 11+fusion.js 17——§4 清单补列 convert/:eid 文字漏列而合计自洽);④M10 域落地 internal/domain/director 单包多文件 28 端点全量(integration 第十链 DirectorChain 扣费对账贯穿;双表通路 generation.ShotView.Tbl 显式传递+DirectorShotVideo;执行期抓获 InsertShot 冻结字面 0 错位存量 bug〔F3 同族——MySQL strict 恒 1265,修后语义登记〕+gorp 无字段列投影教训〔AssetJoined 实修〕;splitEpisodes 双阈值/fusion cost=3/convert 裸 pid 等 quirk 逐字);差异 ①~⑭ 入 §4 M10 节;integCleanup 扩 director 三表+孤儿终验扩七零;§3 Go 已实现 191→219(剩 M5 4/M11 12 共 16);migrations 53 不变,delta 无 0003 | Claude |
| v1.14 | 2026-10-19 | 二十六期(期首核查 A1B3C0 修复 + e2e 基建 + spark 缺口补齐):①期首核查 2026101923(二十五期 6ee480f)——A1 shotCount「修后登记」实为回归(GET /tasks wire=驼峰 shotCount〔TaskWithCount json:“shotCount”,冻结 index.js:230 map 增强同源〕,二十五期误改蛇形 t.shot_count 恒显 0 镜;types Task 接口同错;DashboardView.test fixture 键 shotCount 反证;先红后绿修复+grep 守卫入 ci 3.6)+B 类 3 项回写(二十五期段「镜数恒空显」定性错误权威勘误/eslint+ci.sh 头注标签过时/interface 计数 86 口径);②e2e 基建(Playwright 双配置断言册+截图管线/dev-up env 参数化 qmtv_e2e:3301/e2e-verify 六项自检/docs/test/ 两册/冒烟 8 场景 13 用例全绿);③M13 spark 域补齐(internal/domain/spark 三件+routes 2 条+errRespond 哨兵+service_test/repo_gorp_test/integration 第十六链 SparkChain——e2e 巡检抓获的移植缺口,P0 统计残留第八例,合计 237→239+根 CLAUDE.md 基线行「八轮亲数勘误」);④SparkView faqs 死块填充真实文案(用户裁决);⑤ci 第四档 –e2e(显式 opt-in) | Claude |
| v1.13 | 2026-10-19 | 二十一期(期首核查 A3B4C0 修复 + 质量轨六项全裁落地):①期首核查 2026101920(二十续 dd0e1eb)——A1 Mock 评分 content 维整数除法截断(冻结 Math.round)/A2 runEval SSE done 帧 total 恒 0(Mock int 断言丢失)/A3 评估报告生成时间带时区后缀(%v 打 LocalTime 走 time.Time.String())三处先红后绿修复+B 类回写(API-增量首句 235→237 第三次同型+守卫、PRD M11 ⑧⑭ 补登、stale 注释);②质量轨六项(用户裁决):②api_key 明文回显收敛 json:"-"(前端零消费实证,M12 ④注记更新);③Refund 唯一口形态收口(task refundImage/evaluation 两处/admin ReleaseVideo 域外 release Add 归零+grep 守卫);④Task.ScriptText 指针化(0002 后唯一残余可空列);⑤delta 0003 credit_records.task_id NULL 化(credits 域 ''→NULL 绑定收口,M9 ⑨ 同步终结,打破「无 0003」边界正当推翻);⑥delta 0004 积分/佣金全链 DECIMAL(12,2) 七列(pay 佣金原值入台账亦入余额对齐冻结 REAL 亲和,M8 ①/M7 ⑥ 终结;credits 台账边界 float64 化,billing 价格整数面不动);⑦promptcenter 模板接线收口(shot_image/shot_video/asset_*×3 单级+eval_deep/eval_quick,split 三级沿 SplitTemplate 勿并;差异⑪/M10 ⑪/M11 ⑤ 终结,集成链「配置即生效」断言);集成对账 assertBal InDelta 化+⑤ NULL 行端到端断言;守卫③「gorp 禁指针 db 字段」下 Record.TaskID 保持值类型+COALESCE 读侧 | Claude |
| v1.12 | 2026-10-19 | 二十续(期首核查 A3B4C1 修复 + 第七例亲数勘误 + M11/M5 收官落地):①期首核查 2026101910(十九续 bde1232)——A1 ref-videos 上传绑定结构体漏 form tag(multipart 恒不命中,前端当前未消费);A2 batch-generate ids:[] MySQL IN () 语法错 500 vs 冻结 400「所选资产均已生成」(SQLite 空列表容忍的方言边界);A3 episodes 详情 404 ’episode not found’ vs 冻结 :116 专属 ’not found’(共享哨兵分叉——仅此路由映射);B1 service_test 计数漂移(宣称 15 实 18)+B2 perms_test 口径 14→15(A3 补行未联动)+B3 孤儿终验口径三说并存+B4 M10 差异 ⑮ 补登(NULL vs ’’ 形态+Mock 台账 desc 参数化);②M11 亲数勘误 12→14(viral-analysis 2 条 index.js:490-513 无模块行认领而 §4 清单自列——P0 统计残留第七例,与 M8 同型反向),§3 合计 235→237,根 CLAUDE.md 基线行「五轮亲数勘误」 | Claude |
1. 系统概览
AI 短剧内容生产线(拆解-出图-成片)+ 内容社区 + 积分商业化。单体后端,角色三档:游客(公开面)/ 用户(user)/ 管理员(admin)。
| 项 | 值 |
|---|---|
| Go 后端 | backend/,监听 :3300(与冻结版同端口,前端/代理零改动) |
| 数据库 | MySQL 8 单库 qmtv_main(utf8mb4/InnoDB) |
| 前端 | frontend/ 独立项目(v1.1 立项):初始 = 复制 qmtv/src 原样(行为对等零改动),dev :5173(vite proxy /api /files → :3300);终态 Go :3300 托管 frontend/dist。qmtv/ 冻结区转为对照基准,演进轨道见 §7 |
| 静态 | /files/* ← 素材目录(maxAge 1h);/* ← dist SPA fallback |
| 模型接入 | 管理后台注册 OpenAI 兼容端点(model_endpoints 表),无 Key 全站 Mock,零环境变量接 AI |
模块编号 M1M13(v1.14 起含 M13 火花计划——原 M1M12 对齐冻结代码 12 个路由文件,spark 2 条系漏登第八例补入,全文引用)。「端点数」列 = 冻结目标口径(v1.3 W10 校准:M2/M3 的 P0 统计残留已按冻结实存改;v1.7 十五续:M12 36→39 亲数勘误——冻结三文件 41 条物理 admin 路由,withdrawals 2 条归 M7;v1.8 十六续:M7 12→11 亲数勘误——P0 统计残留第四例;v1.9 十七续:M8 5→8 亲数勘误——P0 统计残留第五例,§4 清单自列 8 而标题/本表少算,与 M7 同型反向;v1.10 十八续:M6 38→29 亲数勘误——P0 统计残留第六例,冻结 index.js 组织段 9+enterprise.js 20=29、§4 清单自列即 29 而标题/本表多算 9,与 M7 同型「标题多算」;v1.12 二十续:M11 12→14 亲数勘误——P0 统计残留第七例,viral-analysis 2 条(index.js:490-513)物理夹在 M5 通知段与 M6 组织段之间不属于任何模块行、§4 M11 清单自列 6+6+2=14——与 M8 同型反向;v1.14 二十六期:新增 M13 火花计划 2 条——e2e 全站巡检抓获 Go 零 spark 路由回溯冻结源码,index.js:739-766「火花计划」段 2 条端点从未入任何模块行亦未计入历史合计(P0 统计残留第八例,与第七例同型「无模块行认领」);「Go 已实现」列 = backend/cmd/server/routes.go 实装口径(2026-10-19 亲数):
| 模块 | 冻结源文件 | 端点数 | Go 已实现 | 职责 |
|---|---|---|---|---|
| M1 认证与用户 | index.js(认证段) | 9 | 9(P1) | 注册/登录/refresh/登出/me/积分流水分页/改密/改邮箱/工作台状态(v1.2 清点修正:原漏登 logout 与 credit-records) |
| M2 任务与拆解 | index.js + routes2 + routes3(task 段) | 38 | 38(P2 W3) | 任务 CRUD、双引擎拆解、资产/分镜/重拆/场次(v1.3 W10 校准:66 系 P0 统计残留,实存 38——含 POST /api/images/storyboard/generate,routes.go 沿冻结物理段归 M2 组) |
| M3 生成与素材 | index.js + routes2 + routes3(生成段) | 19 | 19(P2 W4) | 图片/视频生成、打标、独立工具、音频配音(v1.3 W10 校准:38 系 P0 统计残留,实存 19——含公开面 style-presets/faq 与 GET /api/models/active 补登;storyboard/generate 归 M2 组,剪映属 M4) |
| M4 导出交付 | index.js + routes2 + routes3(导出段) | 10 | 10(P2 W5) | ZIP 交付/剪映草稿/导出任务 |
| M5 通知公告 | index.js | 4 | 4(W20) | 通知/未读数/已读/公告(读侧并入 notification 包——hub 同包零新建) |
| M6 组织协同 | index.js(组织段 :513-:597)+ enterprise.js | 29 | 29(P2 W17) | 组织/成员/素材库/成员组/权限组/看板/作品墙/搜索(v1.10 十八续勘误 38→29:冻结亲数恰 29 条〔组织段 9+企业面 20〕、§4 清单自列即 29——原 38 系 P0 统计残留第六例,与 M2/M3/M12/M7/M8 同型「标题多算」) |
| M7 分销提现 | index.js(v1.8 勘误:11 条端点全在 index.js :598-:638/:708-:737,recharge.js 仅承载返佣写入逻辑属 M8 approve 链) | 11 | 11(P2 W15) | 分销关系/返佣/提现状态机/兑换(v1.8 十六续勘误 12→11:亲数恰 11 条、§4 清单自列即 11——原 12 系 P0 统计残留,与 M2/M3/M12 同型第四例) |
| M8 充值 | recharge.js(8 条全在此文件 :28-:120,挂载点 index.js:27 app.use('/api', recharge.router),admin 4 条系 router 内写死 /admin/ 前缀拼出) | 8 | 8(P2 W16) | 订单/凭证/收款码/审核+两级返佣写入链(v1.9 十七续勘误 5→8:亲数恰 8 条、§4 清单自列即 8——原 5 系 P0 统计残留第五例,与 M2/M3/M12/M7 同型;§4 清单 8 条一直正确而标题少算,与 M7「标题多算」同型反向) |
| M9 剧本库与投稿 | scriptlib.js + studio.js | 25 | 25(P2 W13) | 剧本市集/投稿工作流/编剧档案/解说成片(亲数核验:scriptlib.js 9 + studio.js 16 = 25,v1.3 W10 归属勘误——二十五端点并非全在 scriptlib.js) |
| M10 导演与融合 | director.js + fusion.js | 28 | 28(W18) | 导演工作台/分集分镜/项目资源中心/融合生图 |
| M11 评估与工具 | evaluation.js + workbench.js + index.js:490-513(viral——v1.12 勘正归入) | 14 | 14(W20) | 剧本评估流水线/工作台工具/审片/爆款分析(v1.12:12→14 亲数勘误第七例) |
| M12 管理后台 | index.js(admin) + llmroutes.js + routes2(admin) | 39 | 39(P2 W14) | 用户/积分/公告/组织审批/提现审核/模型接入/计费配置/提示词中心(v1.7 十五续勘误 36→39:41 物理路由 − withdrawals 2 归 M7,instruction-versions 通配展开+DELETE 漏登补齐——原 36 系 P0 统计残留,与 M2/M3 同型;withdrawals 2 端点随 M7 期落地) |
| M13 火花计划 | index.js:739-766(火花计划段——v1.14 补登) | 2 | 2(W26) | join 报名(置位+台账+100+通知)/me 激励投影(submissions 三计数+三阶梯+环形 next_tier)(v1.14:PRD 清单从未登记——P0 统计残留第八例,与第七例同型「无模块行认领」) |
| 基础 | index.js(SSE/health/release-mode) | 3 | 3(P0) | SSE 事件流/健康检查/发布模式 |
| 合计 | 239 | 239 | (v1.14 二十六期校准:M13 火花计划 2 条补登后模块数直加=239〔3+9+38+19+10+4+29+11+8+25+39+28+14+2〕;v1.12 直加=237 系 spark 2 条未入任何模块行的第八例残留——亲数为准。Go 已实现 239 = 全量收官(W26 spark 2 落地;另有 SPA fallback 非 API);v1.10 直加=235 系 viral 2 条未入任何模块行的第七例残留——亲数为准。v1.12「Go 已实现 237=全量收官」系第八例残留口径(v1.14 勘误作废——W20 时 spark 2 条未在册);v1.9「直加=244」含 M6 统计残留未清——亲数为准。Go 已实现 219 = 基础 3 + M1~M4 76 + M6 29(W17)+ M7 11(W15)+ M8 8(W16)+ M9 25 + M10 28(W18)+ M12 39;另有 SPA fallback 非 API) |
2. 全局契约(Go 版逐字节兼容面)
| # | 契约 | 规格 | 违反后果 |
|---|---|---|---|
| C1 | 响应信封 | 成功 {"ok":true,"data":<业务数据>};失败 HTTP 4xx/5xx + {"error":"<中文消息>"}。前端 api.js 解包 .data、抛 {status,message} | 前端全面崩 |
| C2 | 健康检查 | GET /api/health 裸 {"ok":true,"name":"QMTV"}(不走信封) | 监控误判 |
| C3 | 鉴权 | Authorization: Bearer <token>;token query 兜底与公开面见 §2.1 | 401 面 |
| C4 | token query | 冻结实为三分(v1.2 勘误):一般端点 authRequired 仅 Bearer(util.js:49,不认 query);GET /api/events 纯 query(index.js:151);GET /api/images/:id query 优先+Bearer 兜底(index.js:427,单端点内联)。Go 裁决(C3,2026-10-18):FromRequest 统一 Bearer 优先 + query 兜底(全局)——/api/events?token=(SSE)与 /api/images/:id?token=(img 无法带 header)单通道请求全兼容;行为面略宽于冻结(一般端点也接受 query),差异显式登记,守卫 scripts/frozen_contract_check.py 断言三分事实 | 素材 401 |
| C5 | SSE | GET /api/events:text/event-stream,先发 event: connected\ndata: {}\n\n,25s 心跳 : hb\n\n,帧 event: <name>\ndata: <JSON>\n\n;五类事件 video/split/recharge/workbench/eval(payload 见 §4 各模块) | 实时进度失效 |
| C6 | uid 格式 | <前缀>+UUIDv4(带连字符);前端存在 slice 截取依赖(routes2.js:368、enterprise.js:40)。前缀常量表见 §2.2 | 前端截取错位 |
| C7 | 时间格式 | YYYY-MM-DD HH:MM:SS 本地时间字符串(SQLite 遗产,前端按此渲染)。Go 用 DATETIME(3)+DSN parseTime=true&loc=Local+LocalTime 类型序列化,禁 RFC3339 | 时间显示错 |
| C8 | 密码哈希 | scrypt N=16384,r=8,p=1,keyLen=32,salt=16B hex,存储 salt:hash | 存量账号无法登录 |
| C9 | 错误文案 | 中文消息有前端正则依赖(如 /所有者/、/密码/、429 操作过于频繁,请稍后再试、402 积分不足);错误码面(401/402/403/404/429/500/502)与冻结版一致 | 前端提示错乱 |
| C10 | 限流 | 内存桶:register 5 次/h/IP、login 20 次/min/IP(Go 版保持内存实现,集群期集中化) | 行为漂移 |
| C11 | 上传 | multipart,fixFilename latin1→utf8 文件名修复必须移植;模块限额:index 20MB/routes3 50MB/fusion 60MB/workbench 80MB/recharge 8MB/evaluation 10MB/scriptlib 2MB×20/llmroutes 2MB/director zip 20MB;JSON body 5MB | 中文文件名乱码 |
| C12 | JWT | 三段标准 HS256(新签发,exp 秒)+ 两段 legacy 解析兼容(exp>1e12 按毫秒),claims {uid,rol,exp};secret:jwt.secret 配置优先,缺省生成持久化 data/.jwt_secret 等价物(bin 外持久文件) | 全员掉线 |
2.1 鉴权矩阵
| 级别 | 端点 |
|---|---|
| 公开 | POST /api/auth/register、POST /api/auth/login、POST /api/auth/refresh、GET /api/health、GET /api/system/release-mode、GET /api/announcements/active、GET /api/style-presets、GET /api/support/faq |
| token query(Go:FromRequest 统一 Bearer 优先+query 兜底;此二端点为最小依赖端点) | GET /api/events、GET /api/images/:id |
| Bearer(user) | 其余全部 |
| Bearer + admin | /api/admin/* 全部 + 提现审核/充值审核/组织审批/投稿审核等 admin 端点(各模块表内标注 🔒) |
2.2 uid 前缀常量表(infra/ids 集中定义)
u_ 用户、t_ 任务、shot_ 分镜、as_ 资产、img_ 图片、si_ 分镜图槽(index.js:249/:289)、v_ 视频、vid_ Mock 视频落盘文件名(assets.js:39,与行 id v_ 解耦)、cr_ 积分流水、nt_ 通知、rt_ 刷新令牌、e_ 导出(v1.5 勘误:原写 ex_,冻结 index.js:458 实证)、wb_ 工作台任务(workbench.js:55)、ev_ 评估项目(evaluation.js:248)、dpr_ 导演项目(v1.5 勘误:原写 dp_,冻结 director.js:274 + fusion.js:230 实证;含 dir_+projectId 伪外键挂 assets.task_id)、dsh_ 导演分镜(director.js:68)、dep_ 导演分集(director.js:99 + fusion.js:235)、ih_ 图片历史(index.js:324/:356 + director.js:214)、sl_ 剧本库(v1.5 勘误:原写 lib_,冻结 scriptlib.js:131/:222/:247 + studio.js:146 实证)、su_ 剧本使用记录(scriptlib.js:206)、sub_ 投稿(studio.js:53)、wp_ 写手档案(studio.js:90/:143)、na_ 解说任务(v1.5 勘误:原写 nar_,冻结 studio.js:167 实证)、ro_ 充值单(v1.5 勘误:原写 rch_,冻结 recharge.js:40 实证)、up_ 上传文件、fu_ 融合项(fusion.js:160)、rv_ 参考视频(fusion.js:210 + workbench.js:193)、au_ 音频(v1.3 勘误:P0 误写 aud_,冻结 routes3.js:391 实证)、vo_ 配音、tts_ TTS(assets.js:65)、wk_ 作品(v1.5 勘误:原写 w_,冻结 enterprise.js:69 works 实证)、cm_ 佣金(v1.5 勘误:原写 rc_,冻结 recharge.js:72 实证)、w_ 提现(v1.5 勘误:原写 wd_,冻结 index.js:621 实证——w_/wk_ 曾错位)、ex_ 佣金兑换(index.js:634)、va_ 爆款分析(index.js:507)、org_ 组织、pg_ 权限组、pt_ 提示词模板(promptCenter.js:36 冻结实证;Go prompts 内置不读表,存储面见 prompt_templates 补种注记——十四续 B1 补登)、mg_ 成员组、fd_ 素材夹(v1.5 勘误:原写 af_,冻结 enterprise.js:93 实证)、al_ 资产库(routes3.js:75)、an_ 公告(seed.js:24 + index.js:678)、inv_ 组织邀请(index.js:539)、me_ 自定义模型(models.js:19/:61)、oa_ 组织申请(index.js:516)、om_ 组织成员(index.js:547/:698)、st_ 风格(静态 id)、rr_ 审片报告(与 referral_relations 共用前缀——冻结两表均 uid('rr_'),UUID 空间无冲突,v1.2 注记)。
v1.5 已按
grep -ohE "uid\('[a-z_0-9]+'\)" qmtv/server/*.js | sort -u全量复核回填(冻结 47 前缀亲数),Go 侧 51 常量(含基建 4)以backend/internal/infra/ids/ids_test.go全量对拍守卫钉死(TestPrefixes_FrozenContract)。
3. 修正项(冻结行为 → Go 行为)
| # | 项 | 冻结行为 | Go 行为 | 兼容影响 |
|---|---|---|---|---|
| F1 | refreshToken 真轮换 | refresh_tokens 从不 INSERT,/api/auth/refresh 恒 401(靠 72h access token 掩盖) | 登录签发 refreshToken 落库(SHA-256 hash 唯一);刷新=校验(未撤销未过期)→撤销旧+签发新(轮换,replaced_by 串链);登出/改密撤销全部;refresh 响应增 refreshToken 字段 | refresh 端点从必 401 变可用;前端 api.js:24 须消费轮换返回的新 refreshToken——P1 同期补丁(W9a):冻结版前端刷新后复用旧值,真轮换下首次刷新即撤销旧 token,旧前端将在 72h 后被强制登出;存量用户重登一次 |
| F2 | 原子扣款 | tryDeduct check-then-set(SELECT 后 UPDATE)非原子,并发可透支为负 | 单条 CAS:UPDATE users SET credits=credits-?, version=version+1 WHERE id=? AND credits>=?(version 仅自增不入谓词——MySQL 行锁下 credits 谓词已原子防超扣,v1.2 微调),affected=0 判余额不足。v1.3 尾注(P1 实测+P2 定稿):该谓词下 affected=0 ⇔ 余额不足,不存在版本冲突路径——optimistic.RunWithRetry 不引入(qmtv/pms 双侧零生产调用,无原型);跨聚合时序定稿见 §6 | 前端无感;并发下不再超扣 |
| F3 | export_tasks.shot_count | INSERT 引用该列但建表缺失(新库导出必报错的存量 bug) | 建表含 shot_count INT NOT NULL DEFAULT 0 | 修复 |
| F4 | providers 死表 | 建表但无任何代码读写 | 裁剪不建 | 无 |
| F5 | JWT 标准化 | 两段式 body.sig、exp 毫秒、!== 非常量时间比对 | 三段 HS256、exp 秒、hmac.Equal 常量时间;双格式解析一个发布周期(两段+exp>1e12 判毫秒) | 新旧 token 并存过期(≤72h 自然过渡) |
| F6 | 计费双层口径统一入口 | util.COSTS(老接口硬编码)与 system_settings(routes2 覆盖价)并存,视频价两套 | 保留双层语义但统一经 billing 服务读取(COSTS=代码兜底默认,settings 键存在则覆盖);键名清单冻结:trial_credits,text_tokens_per_credit,image_generation,video_pull,video_per_second,image_model,video_model,text_model,image_model_fallback,provider_routing(v1.7 勘误:image_model_fallback 冻结 routes2.js:34 原文名,本行曾误写 image_fallback_model)+ 评估价 eval_quick,eval_deep | 老接口价格若有 settings 覆盖则行为变化(对齐管理端配置的预期语义,PRD 评审已确认) |
| F7 | 邮箱校验统一(v1.2 新增) | register 用损坏正则 /^[^@s]+@[^@s]+.[^@s]+$/(index.js:80——\s 丢失反斜杠,实际仅要求「@ 后非空」,垃圾邮箱可注册);PUT /api/auth/email 用严格版(index.js:130) | 统一严格版 /^[^@\s]+@[^@\s]+\.[^@\s]+$/(单一校验器,register/email 共用) | 垃圾邮箱注册被拒;正常邮箱无感(用户裁决 2026-10-18) |
| F8 | 素材对象存储(v1.2 新增,C4 裁决;v1.3 按落库终态定稿) | 本地磁盘 data/files + /files 无鉴权 static + file_path 落库存 /files/... 相对 URL | infra/storage 三实现:MinIO(192.168.50.190 共享实例,独立 bucket qmtv)/本地磁盘(files.dir 兜底)/Noop(endpoint 空降级 ErrNotConfigured);落库 file_path:MinIO 模式 public_base 非空=终态绝对 URL(C1 裁决,前端逐字消费;桶/域迁移需一次性 UPDATE file_path),public_base 空=相对 /{bucket}/{key};local 模式=/files/ 相对(=冻结形态);ObjectKey 日期分目录+随机 hex 防穿越。v1.3 布局差异登记:冻结产物扁平落 files/(img_x.svg),Go ObjectKey 产 files/yyyyMMdd/x.svg 日期子目录(同 LocalDir 混居)——前端只整串消费 URL 无感,/files 静态映射目录树天然可达 | 前端 URL 消费不变;备份语义变「DB+桶」两件套(DEPLOY 同步);P1 无 HTTP 消费者(M8 留后续期),接口本期定型 |
| F9 | 退款防重唯一口(v1.3 新增,修冻结漏洞) | 退款=addCredits(cost,'release') 散布 providers 三处+启动恢复+管理端;videos.refunded 置位仅覆盖 Mock 失败与轮询失败两路——视频提交失败(providers.js:129)与启动恢复(index.js:800)退款不置位,其后仍满足管理端 status='failed' AND NOT refunded 条件可二次退款;images 无防重列(同步路径天然无二次退款面,维持不设防) | 新增 credits.Refund 唯一退款口:videos 路径先 CAS UPDATE videos SET refunded=1 WHERE id=? AND refunded=0(affected=0 ⇒ 已退,幂等跳过)再退积分;全部失败路径(providers 提交/轮询/下载失败、启动恢复)收口于此。置位与退款两步无事务(禁事务铁律),中断窗口=补偿式一致性既有边界 | 修复双退款漏洞;存量演示库若有「已 failed 未置位」行,启动恢复首跑会补置位+补退款(风险登记 P2 计划) |
4. 端点契约清单(M1~M13 + 基础)
表列:
方法 路径|鉴权(默认 Bearer,🔒=admin,公开面标注 ○)|一句话契约。请求/响应字段以冻结代码为准,FSD 阶段逐模块细化(含字段表/校验规则/错误码)。
基础
| 端点 | 鉴权 | 契约 |
|---|---|---|
| GET /api/health | ○ | C2 裸格式 |
| GET /api/events | token query | SSE 流(C5),连接先发 connected |
| GET /api/system/release-mode | ○ | {ok,data:{mode:'stable'}} |
M1 认证与用户(9,v1.2 清点修正)
| 端点 | 鉴权 | 契约 |
|---|---|---|
| POST /api/auth/register | ○ 限流5/h | 送 200 积分(trial_credits 可配,F6 Billing 读取——冻结硬编码不读 settings,属刻意差异);invite_code 命中建分销关系+邀得 50 积分;referral_code=QM+6位随机大写;F7 邮箱严格版 |
| POST /api/auth/login | ○ 限流20/min | 返回 {token,refreshToken,user}(F1:refreshToken 真实签发) |
| POST /api/auth/refresh | ○ | F1 真轮换;响应含新 refreshToken;停用用户 401(安全收敛) |
| GET /api/auth/me | Bearer | publicUser 字段集:id/email/nickname/role/credits/org_id/workspace_version/referral_code/affiliate_enabled/created_at |
| POST /api/auth/logout | Bearer | 冻结无服务端状态清除;Go F1:撤销该用户全部 refreshToken(v1.2 补登) |
| GET /api/auth/credit-records | Bearer | 全局积分流水分页(size 20,{list,total,page},created_at DESC,id DESC)(v1.2 补登) |
| POST /api/auth/change-password | Bearer | 改密后撤销全部 refreshToken |
| PUT /api/auth/email | Bearer | 需密码确认;F7 严格版 |
| PUT /api/auth/workspace-state | Bearer | 工作台版本状态存 users.workspace_version |
M2 任务与拆解(38,v1.3 W10 校准:标题「66」系 P0 统计残留)
端点实装清单见
docs/API-增量.mdM2 节(38 条逐条)。归组注记(v1.3 W10):POST /api/images/storyboard/generate物理位于冻结 index.js 生成段,routes.go 沿冻结分组归 M2 组(:141)——本节正文按实装口径收录。
CRUD:GET/POST /api/tasks、GET/PUT(title)/DELETE /api/tasks/:id、POST /api/tasks/:id/duplicate。 拆解双引擎:POST /api/tasks/v2(llm.isAvailable→异步 LLM 拆解/Mock 同步,split_status 状态机 idle/processing/done/failed/fallback——注〔三十期〕:idle/fallback 两态冻结与 Go 均无写入点,实现差异见 FSD m02 F2.03 §6;实际三态 done/processing/failed,DDL 默认 done)、POST /api/tasks/:id/resplit(20 积分)、POST /api/tasks/:id/restart、POST /api/tasks/:id/continue(对缺图镜头按 image_cost 补扣)、POST /api/tasks/:id/mark-failed 🔒、POST /api/tasks/:id/scenes/auto-generate(+ /cancel)。 资产:POST /api/tasks/:id/assets/extract 与 /extract2(2K,image_cost×N)、PUT /api/tasks/:id/extract-settings、POST /api/assets/:id/image/upload、POST /api/assets/:id/regenerate、POST /api/assets/:id/derive(衍生变体,parent_id)。 分镜:GET /api/tasks/:id/full 与 /full2(尾帧参考版)、GET /api/tasks/:id/scenes2、PUT/DELETE /api/tasks/:id/scenes/:groupNo、PUT /api/shots/:id、DELETE /api/shots/:id、POST /api/shots/:id/slots、DELETE /api/shot-images/:slotId、GET /api/shots/:id/histories、PUT /api/shots2/:id/params、POST /api/tasks/:id/storyboard-batch(批量补图)、POST /api/images/storyboard/generate(单镜补图,v1.3 W10 归组注记见上)。 资产库联动:GET/POST /api/asset-library、POST /api/asset-library/:id/share、DELETE /api/asset-library/:id、POST /api/tasks/:id/library-apply/:libId。 积分明细:GET /api/tasks/:id/credit-records。
M3 生成与素材(19,v1.3 W10 校准:标题「38」系 P0 统计残留——render-all 实属 M4、retry 无用户端点)
端点实装清单见
docs/API-增量.mdM3 节(19 条逐条)。POST /api/images/storyboard/generate归 M2 组(见 M2 归组注记)。
图片:POST /api/images/generate(COSTS.image,失败 providers 内部退款)、GET/DELETE /api/images/:id、GET /api/images、POST /api/images/upload。
视频:POST /api/videos/generate(COSTS.video)、POST /api/videos/generate-v2(videoCost(secs)=video_pull+per_second×secs,secs 1-15,reso 720p/1080p)、POST /api/videos/:id/cancel(cancelled 不退款)、POST /api/videos/tag(10×镜数)、GET /api/videos、GET /api/standalone-videos。
音频:GET/POST /api/audios、DELETE /api/audios/:id、POST/GET /api/videos/:id/voiceover(Mock WAV 字节级兼容)。
公开/辅助面:GET /api/style-presets ○(19 种风格预设)、GET /api/support/faq ○(静态 6 条)、GET /api/models/active(getBilling 投影——v1.3 W10 补登,PRD 原文全篇未列该端点;消费方 DashboardView/VideoView/Director3DView 三处〔三十期勘正,Director3DView 系 M10 落地后新增〕)。
异步契约:有 Key 时 video 提交→轮询(8s 首查/10s 步进/90 次上限)→SSE video 事件(done/progress/failed);失败 refunded=1 防重退款。
实现差异(v1.3 W10 入正文,Go 实装 vs 冻结行为,全部显式登记):①expires_at=C7 例外全达成(冻结 toISOString() 入库透传;Go ISOTime 响应 ISO、落库仍 DATETIME(3));②SmartVideo ⑩ 分辨率白名单外值(如 480p)冻结原样透传 vs Go 归一 1080p、ratio null 冻结传 null vs Go 归一 16:9(Go 收紧向,安全);③SSE progress 冻结每次 fetch 前发 vs Go fetch 后发(tries 命中集合相同);④tag 打标价冻结固定 COSTS.image=10 vs Go 随 F6 settings 覆盖价。
M4 导出交付(10)
POST /api/export(2 积分×完成段数,shot_count)、GET /api/export/:id(导出任务轮询面)、GET /api/export/task/:taskId(查重防重复扣费)、GET /api/tasks/:id/export/assets-deliver(2K 资产 ZIP)、GET /api/tasks/:id/export/deliver(全量 ZIP)、POST /api/tasks/:id/export/tag-all、POST /api/tasks/:id/export/render-all(Σ videoCost 批量渲染)、GET /api/tasks/:id/export/history、PUT /api/tasks/:id/export/scenes/:sid、GET /api/tasks/:id/jianying-draft(剪映草稿 JSON)。
实现差异(v1.3 W10 入正文):①history 响应 v.* 列序——冻结 created_at 在 error 之后(db.js CREATE 后 ALTER 折叠序)vs Go DDL/struct 排末位:键集一致、前端按键取值无感,严格逐字节对拍不齐;②assets-deliver 条目名 index_no=0——冻结 JS truthy→asset_ vs Go != nil && != 0→shot000_(W6 期已修对齐冻结 truthy 语义);③export repo 接口实为 16 方法(W5 基线「17」系计数漂移);④F3 修正兑现——冻结 export_tasks 无 shot_count 列而 INSERT 显式引用(导出必报错存量 bug),Go DDL 含该列按修后语义(P0 裁决)。勘误(v1.3 W10):本节原列「GET /api/export」系 P0 清单误计——冻结实存的是 GET /api/export/:id 轮询端点,无列表端点。
M5 通知公告(4)
GET /api/notifications、GET /api/notifications/unread-count、POST /api/notifications/read、GET /api/announcements/active(○;前端 localStorage qmtv_dismissed 由前端自理)。
✅ M5 落地(2026-10-19 W20,
internal/domain/notification读侧——hub 同包零新建):App.vue 铃铛三面/AccountView 摘要 404 转绿(前端零改动)。实现差异登记:①readMySQL 保留字全 SQL 反引号(auth/repo.go:130 先例同制);②POST read 无 id=整用户全标已读+恒 ok(true) 无 404 面(id 不属于自己也静默——冻结 :480-483 quirk 照抄);③announcements/active 公开端点无 amw(冻结 :485 无 authRequired);④通知零 SSE(前端 60s 轮询 unread-count)+读侧行形态兼容五源写入(auth/admin/recharge/seed/evaluation 自持 INSERT)。 | Claude |
M6 组织协同(29,v1.10 十八续勘误:标题「38」系 P0 统计残留——冻结亲数恰 29 条〔index.js 组织段 9 + enterprise.js 20〕,下清单即全量;与 M7 12→11 同型「标题多算清单对」第六例)
组织:POST /api/organizations/applications、GET /api/organizations/me、PUT /api/organizations/me/credit-allocation-mode(pooled/independent)、POST /api/organizations/me/invites、POST /api/organizations/join、DELETE /api/organizations/me/members/:memberId、PUT /api/organizations/me/members/:memberId/role(非 owner 拒,文案含「所有者」)、POST /api/director3d/:id/share、GET /api/organizations/shared-projects。 企业:GET /api/organizations/me/dashboard(冻结积分+14 天趋势+双排行)、GET/POST /api/showcase、DELETE /api/showcase/:id、素材库目录树 GET/POST /api/organizations/me/material/folders(+DELETE :id)、GET /api/organizations/me/materials、POST /api/organizations/me/material/assign、成员组 GET/POST /api/organizations/me/groups(+DELETE)、权限组 GET/POST/PUT/DELETE /api/organizations/me/perm-groups*、PUT /api/organizations/me/members/:memberId/assign、GET/PUT /api/organizations/me/material-perms*、GET /api/search(全站搜索)。 容量:素材 10GB 配额(images.file_size/audios.size 聚合)。
✅ M6 落地(2026-10-19,
backend/internal/domain/org单包多文件七件 29 端点全量):integration 第九链 OrgChain(M12 approve 建组织→invite/join 全链→角色面三哨兵→dashboard available==balance 钉死→material-perms 联动端到端→showcase 恒 ok→share/search/401 面)。实现差异登记:①credit-allocation-mode 死端点照抄(前端零消费方,mode 非 independent 一律 pooled);②dashboardavailable恒=balance 无任何过滤+frozen 仅 eval_projects status=‘running’(M11 未落地恒 0)+projectRankdir_前缀死分支(实产 dpr_ 永不命中且 slice(4) 偏移亦错)——三处冻结谎言/死分支照抄不修,集成链钉死;③works.cover_path/asset_folders.org_id/member_groups.leader_id 等 Go DDL NOT NULL DEFAULT ’’ vs 冻结可空——读侧 NULLIF 还原 null(recharge ⑦ 先例);④素材 10GB 配额系估算法非真 SUM(images.sum(file_size)+NULL 数×80KB+audios.sum(size)+videos 数×2MB 系数硬编码,plan 恒「基础档」,percent toFixed(1) 等价 Round(pct×10)/10);⑤org_applications company/contact/reason 冻结不 slice 不 trim——Go VARCHAR(128) strict 溢出报错(SQLite 动态类型照收),超长输入行为差异;⑥invite code 冻结 Math.random base36 截 8 位大写 vs Go crypto/rand 同字母表(形态等价 ^[A-Z0-9]{8}$,值域随机性不属契约面);⑦organizations.credits/org_members.quota/credit_allocation_mode 三列冻结无读写消费方(pooled/independent 分账完全未接线)——空转照抄;⑧DELETE /showcase/:id 与 DELETE /organizations/me/groups/:id 不查行数恒 ok:true(删他人/不存在均静默);groups DELETE 无 404 面(403 后恒 ok);⑨members target 不存在与 target 是 owner 合并同一 400「无法移除」(非 404 quirk);⑩invites 文案「仅组织管理员可邀请」而判定 role=‘owner’ 严格(quirk 照抄,:553 移除面同款);⑪PUT members/:memberId/assign 响应 group_id/perm_group_id 缺省时冻结回nullvs Go 回""(响应面未走读侧 NULLIF 还原——前端 assign 后重拉列表非回归,十九续 B3 登记)。十九续勘正(2026-10-19):dashboard projectRank 谓词十八续误加task_id != ''曾把空串消耗聚合行(冻结 :37 IS NOT NULL 放行,以缺省名「独立生成 / 工具」kind=工具 GROUP BY 上榜)整行挡掉——已还原逐字+集成钉守(A4);shared-projects video_id 谓词同批还原IS NOT NULL(MySQL NOT NULL 列上即冻结字面);shared-projects 投影补script_text(冻结dp.*全列,组织共享面下发全剧本即冻结语义,用户裁决补齐);projectRank wire 键json:"amount-kind"勘误kind(A1,前端 OrganizationView 「类型」列曾恒空白——wire 键集断言守卫自此入惯例)。 | Claude |
M7 分销提现(11,v1.8 十六续勘误:标题「12」系 P0 统计残留——冻结亲数恰 11 条〔下清单即全量〕,与 M2 66→38/M3 38→19/M12 36→39 同型第四例)
GET /api/distribution/overview、GET /api/distribution/downlines、GET /api/distribution/records、GET/POST /api/distribution/withdrawals(状态机 pending→reviewing→topay→paid→done / rejected / cancelled)、POST /api/distribution/withdrawals/:id/cancel、POST /api/distribution/withdrawals/:id/confirm、GET/POST /api/distribution/exchanges、PUT /api/admin/withdrawals/:id 🔒、GET /api/admin/withdrawals 🔒。返佣:充值到账两级返佣(commission_rate/level2_rate 可配,默认 8%/2%);满 980 元自动开 affiliate_enabled。
✅ M7 落地(2026-10-19,
backend/internal/domain/distribution单包多文件 11 端点全量):integration 第七链 DistributionChain(seed 对拍 40 佣金/0.08-0.02-100/状态机全迁移/cancelled·rejected 不释放额度端到端钉死/exchanges 占扣/403/404 面)。实现差异登记:①withdrawals/exchanges 冻结的是佣金台账而非 users.credits——POST 两写面不触 credits/credit_records/通知/SSE,域零 credits 依赖(返佣写入方在 M8 approve 链,未落地前仅 seed 行可达);②available 分母SUM(withdrawals.credits)无 status 过滤——cancelled/rejected 工单永不释放额度,:715 note「收益已解锁」与行为相悖系冻结谎言照抄不修(集成链 ⑨⑩ 双断言钉死);③overview pending_withdraw 冻结双 IN 冗余(四态 AND 三态)→ Go 等价单 IN 三态(结果逐位一致,repo 注释钉行号);④settings 回退三体 quirk:overview 无行回退{0.2,0.05,100}(无 id 键)与 POST 提现面回退仅{min_withdraw:100}各自独立(Go 两 shape 不共用),种子行 0.08/0.02/100 覆盖表默认;⑤affiliate_threshold 硬编码 980(非 system_settings affiliate_threshold_cny,与 recharge.js:25 不同源)照抄;⑥credits 请求绑定 float64(冻结+req.body.credits强转)入库 int64(MySQL BIGINT 舍入 vs SQLite 动态类型可存小数——前端常态整数无差)——二十一期⑥ DECIMAL(12,2) 收口(0004):withdrawals/exchanges credits 原值入库,差异终结;⑦reviewed_at 冻结 now()=UTC ‘YYYY-MM-DD HH:MM:SS’ 字符串 → Gotime.Now().UTC()显式(与 auth LocalTime 本地时先例刻意不同源);⑧admin PUT 状态机 W_FLOW 严格邻接校验禁跳转(终态无键恒 400),动态文案不允许从「X」流转到「Y」W_ZH miss 原样回显英文态;⑨exchanges.status 恒 ‘pending’(全冻结无 UPDATE,死态照抄);⑩withdrawals 2 条 admin 端点在本域(M12 裁决归 M7),AdminView 提现 tab 404 面转绿。
M8 充值(8,v1.9 十七续勘误:标题「5」系 P0 统计残留第五例——清单自列恰 8 条而标题少算,与 M7「标题多算」同型反向)
✅ M8 落地(2026-10-19,
backend/internal/domain/recharge单包多文件 8 端点全量):integration 第八链 RechargeChain(config 对拍/下单 rate 换算/凭证真 multipart 落盘/approve 到账对账+两级返佣 8·2/资格联动 ≥980 自动开通+已开不重复/reject 双截断/admin list status 过滤/403/404 面);integCleanup 扩 distribution 四表+recharge_orders(十七续 B2 收口,M8 链起共享清尾)。前端零改动(RechargeView/AdminView 充值面 404 转绿)。实现差异登记:①commission_records 唯一写入方在本域 approve 链(M7 差异①就此收口),order_amount/commission 绑定 float64→MySQL BIGINT round——冻结 SQLite INTEGER 列收 REAL 靠亲和性存原值(如 33 元二级佣金冻结存 0.66 并入账 0.66)——二十一期⑥ DECIMAL(12,2) 全链收口(0004):pay 佣金原值入台账亦入余额,差异终结;②reviewed_at/reviewed_by——冻结 now()=UTC ‘YYYY-MM-DD HH:MM:SS’(recharge.js:93 与 M7 同源)→Go time.Now().UTC() 显式(M7 差异⑦同制);③credits.Add ErrNoUser 显式 vs 冻结 addCredits 静默(util.js:66)——approve 链容错对齐(返佣 inviter 不存在冻结照写台账继续),其余 DB 错误照冻结 throw 传播;④settings 值非数字冻结 NaN→JSON null,Go 回退默认(settings 经 admin 白名单管理常态数字串不可达);⑤affiliate_threshold 读 system_settings affiliate_threshold_cny 默认 980——与 M7 overview 硬编码 980 两处刻意不同源(M7 差异⑤),勿统一;⑥approveOrder 冻结导出「供重启复用」系死代码零消费方——Go 不建恢复链;⑦proof_path Go DDL NOT NULL DEFAULT ’’ vs 冻结可空——读侧 NULLIF 还原 null 透传(export normalizeExportTask 先例同制);⑧二级返佣 from_user_id 恒=充值人(冻结 :78/:80 两级 pay 均传 o.user_id,非链上级)。
GET /api/recharge/config(1 元=N 积分 settings 面+收款码+推广阈值 980 默认)、GET/POST /api/recharge/orders(金额两位舍入/≥1 元/≤10 万;orderNo=‘RCH’+毫秒+3 位随机)、POST /api/recharge/orders/:id/proof(凭证上传 8MB,multer 两路由共用限额)、POST /api/admin/recharge-orders/:id/approve 🔒(到账+两级返佣+SSE recharge 事件+单笔达标自动开推广资格)、POST /api/admin/recharge-orders/:id/reject 🔒(note 落库截 200/通知截 100 缺省「凭证有误」两处不同源)、GET /api/admin/recharge-orders 🔒(status 过滤,‘全部’ 或空=不过滤,LIMIT 200)、POST /api/admin/pay-qrcode 🔒。
M9 剧本库与投稿(25)
Go 实现现状(v1.3 W10 盘点,P2 排期范围外):M9 全部 25 端点 Go 零实现——前端剧本库弹窗/市集对 Go 后端 404 系排期现状非回归(迁移前后行为一致)。归属亲数勘误:scriptlib.js 9(genres/list/:id/use 四件 + admin script-library GET/POST/PUT/DELETE 四件 + upload 一件)+ studio.js 16(market/scripts 二件、投稿 submissions 四件、编剧 writer-profile 二件、writers 二件、admin/submissions 审核二件、admin cover+rating 二件、tools/narrations 二件)= 25。
✅ W13 落地(2026-10-18,
backend/internal/domain/scriptlib25 端点全量):routes.go M9 组恰 25 条亲数;AdminMiddleware 接线 9 admin 端点(403 forbidden;十四续 B3 勘误「8」=scriptlib 5+studio 4 亲数);integration 第五链 ScriptlibChain(ensureSeed 首触/submissions 全链/admin review publish 多表写/narrations 扣费 30 对账恒等式);前端消费面 URL 不变零改动(Dashboard 弹窗/ScriptLibraryView/StudioView/WritersView/AdminView/IndependentToolsView 404 面转绿)。实现差异登记:①admin upload 修后语义——冻结 scriptlib.js:248 run 传 16 参数对 9 占位符,better-sqlite3 绑定超占位恒 throw RangeError(已实测坐实),冻结版 upload 端点从未可用,Go 按 9 列 INSERT 修后实现(F3 同制);②cover_path 落库走 F8 口径(MinIO 绝对 URL/local /files/ 相对),冻结恒 /files/ 相对;③upload GBK 回退用 GB18030 解码(WHATWG TextDecoder(‘gbk’) label 实走 gb18030 算法);④upload/cover 限额=路由级 BodyLimit(45mb/10mb)+handler 单文件校验(2MB/超 20 文件按 multer 语义 500),冻结 multer fileSize 单文件限制;⑤length()→CHAR_LENGTH(MySQL length 为字节数);⑥char_count/preview/catalog 截断按 rune(JS UTF-16 BMP 等价,gateway.Err 300 rune 先例同制);⑦go.mod 无新增依赖(golang.org/x/text 已在 indirect 转直接);⑧writers 列表 search Go 侧 TrimSpace 后 LIKE(冻结 studio.js:105 直用原值不 trim)——用户裁决保持+登记(检索体验更稳;market 端点 search 与冻结一致不 trim,两处刻意不同源,repo.go 注释在);⑨narrations 扣费台账 task_id 落''而非冻结 null(credits.Record.TaskID 为 string 非 *string,沿既有 credits 模型形态——二十一期⑤已收口:0003 列 NULL 化+域内''→NULL绑定;冻结 studio.js:158 tryDeduct 第 4 参 null)——credit-records 响应形态微差,待 credits 模型指针化时一并(十四续 B7 正文差异清单补登,v1.5 修订行已有同义注记)。
市集:GET /api/script-library/genres(含 markets/audiences/forms 配置)、GET /api/script-library(五维筛选/排序)、GET /api/script-library/:id(目录+试读)、POST /api/script-library/:id/use(7 日热度)、GET /api/market/scripts、GET /api/market/scripts/:id。种子幂等回填逻辑(ensureSeed)保留。 投稿:GET/POST /api/studio/submissions、PUT /api/studio/submissions/:id、POST /api/studio/submissions/:id/submit(draft→pending)、编剧档案 GET/POST /api/studio/writer-profile、GET /api/writers、GET /api/writers/:id。 管理:GET/POST /api/admin/script-library、POST /api/admin/script-library/upload(20 文件×2MB)、PUT/DELETE /api/admin/script-library/:id、POST /api/admin/script-library/:id/cover、PUT /api/admin/script-library/:id/rating、GET /api/admin/submissions、POST /api/admin/submissions/:id/review 🔒(needs_change/rejected/published;publish 自动建档+入库)。 解说:POST/GET /api/tools/narrations(30 积分,Mock WAV)。
M10 导演与融合(28)
导演:GET/POST /api/director3d、DELETE /api/director3d/:id、GET /api/director3d/:id、POST /api/director3d/:id/resplit、GET /api/director3d/episodes/:eid、POST /api/director3d/:id/episodes/:eid/extract-assets、POST /api/director3d/:id/episodes/:eid/shots(确定性 9 字段分镜)、PUT /api/director3d/shots/:id、POST /api/director3d/shots/:id/generate-image(-video)、GET /api/director3d/:id/export(ZIP)、POST /api/director3d/import。双表通路:director_shots 与 shots 并存,providers 按 shot._tbl 路由(Go 版以表名字段显式传递)。
融合:GET/POST /api/director3d/:id/assets、POST /api/director3d/assets/:id/generate(3 积分)、POST /api/director3d/:id/assets/batch-generate、POST/DELETE /api/director3d/assets/:id/queue*、GET /api/director3d/:id/queue(持久化队列 recoverQueue)、POST /api/director3d/:id/assets/import、POST/GET /api/director3d/:id/fusion*(融合生图)、POST /api/director3d/:id/remix(30 积分一键转剧)、GET/POST /api/director3d/:id/ref-videos*、DELETE /api/director3d/ref-videos/:id、POST /api/director3d/:id/convert/:eid(30 积分一键转剧——v1.11 勘正补列,原清单文字漏列而合计 28 自洽)。
✅ M10 落地(2026-10-19,
backend/internal/domain/director单包多文件 28 端点全量〔director.js 11+fusion.js 17——亲数 11+17=28 清点清白〕):integration 第十链 DirectorChain(建项目→分集→分镜→分镜图/视频扣费对账恒等式贯穿→资产提取/生成/队列→融合/改创→参考视频→export ZIP 魔数→import ZIP→一键转剧→resplit→属主 404/401)。实现差异登记:①splitEpisodes 双阈值(resplit 1200/fusion CreateProject 800)刻意不同源勿统一(合参参数化);②splitScenes 锚点前残余段亦成场景(「第一集 开端」头成 scene 目标——冻结 lookahead split 语义照抄,集成实测 5 目标坐实);③「道具:」的「道具」二字亦命中 character 正则(冻结 :140 quirk 照抄);④InsertShot 冻结字面 0 错位(:179 列 15 VALUES 16——字面 0 落 resolution、attempts 落 resolution 字符串,SQLite 动态类型容忍的存量 bug;F3 同族)——MySQL strict 恒 1265,Go 按修后语义 15 列 15 ?(resolution=草稿值/attempts=0)登记;⑤videoSmart Mock 分支 UPDATE shots 硬编码(providers.js:91 真实分支才按 tbl 路由)——导演分镜 Mock 生成后分镜行不回填(0 行静默),Go 按 tbl 意图语义打 director_shots(ShotView.Tbl 显式传递——PRD 双表通路约定落地);⑥episodes 面 eid↔project 无关联校验(ep 按 eid 查活、project 按属主查——跨项目 eid 可达 quirk 照抄);convert 例外(:256 WHERE id AND project_id 关联逐字);⑦fusion cost=3 / convert flat=30 硬编码(非 ImageCost 面)+convert taskId=裸 p.id(非 dir 前缀,:259 照抄);⑧convert 空场次:冻结 scene.split TypeError 恒 500(fallback 对象调 split)——Go 按意图语义以 ep.title 单场生成登记;⑨extract-assets 逐张生成失败 continue 不中断(冻结 providers.imageSmart 内部退款等价形态——域内 Refund 唯一口退单价 nil claim,调用方不重复退);⑩assets/images 行 task_id=‘dir’+project_id 约定(M6 projectRank 聚合面依赖);⑪promptcenter 模板查库面沿 M2 同源不查 prompt_templates(内置默认渲染——getShotImagePrompt 冻结有 getActive 查库,M2 起未接线,差异沿袭)——二十一期⑦已接线收口(shot_image/shot_video/asset_*×3 单级查库+内置默认回退,split 三级沿 SplitTemplate);⑫buildShotFields JSON 键序 Go map 字典序 vs JS 插入序(JSON 对象语义等价,前端按键取);⑬AssetJoined 等 JOIN 投影不选无字段列(gorp 反射报错——M6 GetWriterJoinedAt 同族教训,集成链抓获实修);⑭multipart 两面 import 20MB/ref-videos 60MB(up60 新档位;upload 处理器内单文件上限双保险);⑮空串形态与 Mock 台账 desc 两处漏登补记(二十续 B4):assets/ref_videos 的 description/note 空串冻结恒写 ‘’(fusion.js:41/:212)vs Go 落 NULL(wire null vs “"——前端||''消费无感);videoSmart Mock 失败退款台账 desc 参数化「导演分镜视频失败自动退回」vs 冻结硬编码「视频生成失败自动退回」(providers.js Mock catch——⑤只登记了 UPDATE 表名面);冒烟面 404 属主过滤(英文短文案)+403 episodes/extract 属主面逐字。 | Claude |
M11 评估与工具(14——v1.12 勘正:evaluation.js 6+workbench.js 6+index.js viral 2;原清单 :208 自列 6+6+2=14 而标题少算,P0 统计残留第七例与 M8 同型反向)
评估:GET /api/evaluations/config、POST /api/evaluations/upload(txt/md/docx,UTF-8/GBK 自动识别,10MB)、GET /api/evaluations、GET /api/evaluations/:id、POST /api/evaluations(quick 100/deep 300 可配,8 角色×3 阶段流水线,冻结 cost 语义)、GET /api/evaluations/:id/report(txt 下载);SSE eval 事件(stage/done/failed);失败全额退款。
工作台:POST /api/workbench/tasks(转绘 20/去字幕 10/转高清 15)、GET /api/workbench/tasks(recoverWorkbench)、POST/GET /api/tasks/:id/review*、POST/GET /api/director3d/:id/review*(审片 10 积分,双入口共用 review_reports,scope classic/director)。
爆款分析:GET/POST /api/viral-analysis(COSTS.viral=5)。
✅ M11 落地(2026-10-19 W20,
internal/domain/evaluation单包多文件 14 端点全量〔workbench 6+eval 6+viral 2——v1.12 第七例勘正〕):integration 第十一链 EvaluationChain(config 对拍→upload 真上传→扣 100 台账逐字+taskID=ev id→Mock 异步收口 waitFor done 不退款→report 下载头+体→列表定列/详情全列→404 双哨兵)+第十二链 WorkbenchNotifyChain(workbench 守卫三面→video_id 引用 done 成片 Mock done 扣 20 对账→审片双入口 404 两文案分叉+扣 10 台账+空分镜先退对冲→viral 扣 5→M5 四端点 unread-count 联动+read 单条/全标+announcements 公开/401)。实现差异登记:①costOf settings 值为 0 也回退默认 quirk 照抄(冻结+r.value||def);②engine 两字面不同源勿统一(config『规则(Mock)』vs POST『规则』);③mode 非法静默归 quick 无 400;④runEval finished_at UTC 形态照抄(冻结 :181 toISOString 变形——与 created_at localtime 分裂同源)+Mock 节奏 sleep 1100/1500/900 照抄(mockSleep 可注入);⑤LLM 通道 evalLLM 接线(modelCfg 三元+llm.Client.Chat——M12 反引号修复后已生效),模板查库不接线沿差异⑪先例(EVAL_PROMPT 兜底)——二十一期⑦已接线(eval_deep/eval_quick active 查库,content 空回退 EVAL_PROMPT;写侧 CATEGORIES 八键无此二键入口=冻结同 quirk 照抄);⑥workbench watchVideo 状态丢失/tries>180 不 emit SSE 照抄(冻结 :84 无 bus.emit)+超时不退款照抄(providers 已退语义——与 generation PollTimeout 先例不同源勿统一);⑦审片零 SSE 零通知+reviewShots target/scope 死变量不建+classic ’task not found’ vs director ’not found’ 404 文案分叉照抄+空分镜先退台账「审片退回:无分镜(+10)」逐字;⑧viral Math.random→math/rand+五列 INSERT(Go DDL status/version 惯例列默认)+title slice255 防御截断(冻结 SQLite VARCHAR 无约束存全量——MySQL strict 1406 防护,方言适配,二十一期 B2 补登);⑨recoverWorkbench 整体迁移 evaluation.RecoverStuck(recovery/repo.go 预定注释兑现——四方法+BenchRef/BenchFailedErrorText 移除、WorkbenchRecoverer 窄接口注入;装配序教训:deps.eval 必须先于 recovery 建立的首跑 nil panic 被集成 RecoveryChain 抓获);⑩workbench POST 双载荷形态(multipart 80MB up80 新档/JSON——WorkbenchView :131/:133 两形态)+taskId 无=空串(冻结 undefined——narrations ⑥ 同族沿袭);⑪审片跨域共表读 COALESCE(image_prompt,’’)(冻结||''消费语义——集成链 NULL 直插行抓获存量炸点:gorp Scan NULL→string 恒错,shots 表真实 NULL 行同炸);⑫workbench_tasks 可空列裁决=不收紧(source_path/video_id 二选一语义可空;DDL NOT NULL DEFAULT ’’ 即冻结 NULL 形态——wire null vs "” 随 M10 ⑮ 口径),delta 无 0003 维持;⑬GET /workbench/tasks out_url 双条件冗余+...w展开 JOIN 伴列(out_path/video_status)照抄;⑭LLM 边缘面两处微差(二十一期核查 B4 登记):parseEval 重复 dims key 累加 vs 冻结find首个(evaluation.js:125——仅 LLM 畸形输出可达);报告模板缺失键%v→<nil>vs 冻结||''(:271——Mock 全键不达)。 | Claude |
M12 管理后台(39)
用户与积分:GET /api/admin/summary、GET /api/admin/users、PUT /api/admin/users/:id、POST /api/admin/users/:id/password、GET /api/admin/credit-grant-records、POST /api/admin/credit-grant-records、GET /api/admin/credit-records/export(CSV)、GET /api/admin/runtime/tasks(运行时任务观测)。
生成审计:GET /api/admin/generation-audit/failures、POST /api/admin/generation-audit/videos/:id/release(人工退款,校验 refunded 防重)、POST /api/admin/generation-audit/tasks/:id/retry-split。
公告:GET/POST /api/admin/announcements、PUT /api/admin/announcements/:id。
组织审批:GET /api/admin/organization-applications、POST /api/admin/organization-applications/:id/approve(-reject)。
计费配置:GET/PUT /api/admin/credit-settings(白名单键,见 F6)、GET/PUT /api/admin/model-settings。
模型接入:GET/POST /api/admin/model-endpoints、PUT/DELETE /api/admin/model-endpoints/:id、POST /api/admin/model-endpoints/:id/activate、POST /api/admin/model-endpoints/:id/test(text→chat 8 token/image→红圆真生成/video→可达性,45s 超时;api_key 列表掩码前 6 位)。
提示词中心:GET/POST /api/admin/prompts、POST /api/admin/prompts/upload、PUT/DELETE /api/admin/prompts/:id、POST /api/admin/prompts/:id/activate(互斥生效版);指令版本 GET/POST /api/admin/instruction-versions、PUT/DELETE /api/admin/instruction-versions/:id、GET /api/admin/instruction-versions/active、POST /api/admin/instruction-versions/:id/activate(v1.7 通配展开补登 PUT/DELETE :id 两条;DELETE 404 not found 与 prompts DELETE 的 版本不存在 刻意不同源照抄)。
Go 实现差异登记(v1.7 W14,internal/domain/admin 单包多文件沿 M9 先例):①withdrawals 2 端点(GET/PUT /api/admin/withdrawals)随 M7 期落地,本期 404 系排期现状非回归;②死端点照抄:GET/PUT /admin/model-settings、GET /admin/instruction-versions/active 前端零调用(契约对齐,防演进踩空);③ErrNoUser 吞形:grant/release 传不存在用户冻结 addCredits 静默 no-op 通知照发——Go credits.Add 显式 ErrNoUser 由管理台吞掉保冻结信封;④api_key 列表明文回显照抄(冻结 models.js:36 含原文+掩码双键)——管理台泄漏面,沿 C2 先例登记安全注记,未来收敛另立修正项(二十一期②已收敛:EndpointRow.APIKey 改 json:"-" 仅下发 api_key_masked/has_key——前端 AdminView.vue:206 只消费 masked 零消费实证,wire 与冻结分叉一处);⑤instruction-versions DELETE 按修后语义打 prompt_templates(404 not found/400 文案逐字保持)——冻结查删 instruction_versions 死表(迁移后恒空→恒 404),F3 同制;⑥跨域共表写归表主域:retry-split 经 task.AdminRetrySplit(不删根资产=冻结原样,重复重拆根资产重复系冻结行为)、release 认领经 generation.ClaimManualRelease(CAS refunded=0+CONCAT(COALESCE(error,’’)) 追加一步,修冻结先查后改的并发窗,F9 同向);⑦读侧衔接零失效逻辑与冻结同构,但 P0 起 modelCfg.setting() key 未反引号恒 1064——读侧配置链全哑的存量真 bug 由本期集成断言抓获并修复(见 v1.7 修订行④);⑧prompt_templates 面不做 in-band ensureSeed(冻结 promptCenter.ensureSeed legacy 迁移在 Go 无源库):cmd/seed 表级哨兵启动补种 5 行已覆盖,管理台 CRUD 纯存储面;⑨providers/publish 反向死调用不实现=echo 404 兜底即冻结形态(AdminView 对应调用在冻结版同样无后端端点,前端零改动面)。
M13 火花计划(2——v1.14 补登:index.js:739-766「火花计划」段;P0 统计残留第八例,与第七例同型「无模块行认领」——二十六期 e2e 全站巡检抓获 Go 零 spark 路由后回溯冻结源码定界)
报名激励:POST /api/spark/join(joined 守卫 400「您已加入火花计划」逐字 → users.spark_joined 置位 → 台账 kind=‘spark’ desc「加入火花计划奖励(+100)」→ 通知「火花计划报名成功」→ {joined:true,bonus:100});GET /api/spark/me(submissions 三计数:total 系 status!=‘draft’ / published / pending;三阶梯投影 min-max-per-name-perk〔星火 1-2 部 100/炬火 3-5 部 150/燎原 6+ 部 200〕+ reward_earned=published×per + next_tier 环形取下一档——燎原的下一档回星火)。
Go 实现(v1.14 W26,internal/domain/spark 单包三件):逐字对齐无差异;errRespond 哨兵 ErrAlreadyJoined→400;集成第十六链 SparkChain(初态星火回退→join 三动作对账→重复 400→阶梯联动 submissions 直插→401 面)。
5. 数据模型概览
52 表终态(qmtv_main 单库)——分类/逐表裁决/视图清单见 docs/数据库表设计-qmtv.md(W9);DDL 权威 = backend/migrations/full/qmtv_main_full.sql。状态表 22 张(version 乐观锁+entity_lifecycle_event 载体)、日志台账 4、字典配置 6、技术 1(refresh_tokens 重构)、资源明细 15、新增基建 4、裁剪 1(providers)。
P2 DDL 收紧(delta/0002,2026-10-18 落地):tasks 5 可空 TEXT 列+shots 2 prompt 列 UPDATE 回填后 MODIFY NOT NULL(MySQL TEXT 无字面 DEFAULT——漏写列 strict mode 显式红=守卫语义);seed.go tasks INSERT 全列写恒 '';C7 例外不动(videos.prompt/error 保持 *string NULL 透传冻结语义);director_shots(M10)/workbench_tasks(M11)待域落地随域裁决。seed Go 版一步到位差异:producer@xingmang.cn 独立演示账号(referral_code=QMXINGMANG01,与 demo 并存)+台账单笔 init(沿 SQL seed 惯例;冻结 demo 为 0 元 register 标记行形态)+prompt_templates 表级 COUNT 哨兵补种 5 行模板(Go prompts 内置不读表,纯存储面对齐产物无差)。恢复扫描窄投影:task 恢复 SELECT * 遇可空列 NULL 整段静默 no-op 已修(SplitRecoverRow 5 列窄投影,语义等价)——W8 集成抽样抓获。
6. 非功能约束
- 性能:SSE 心跳 25s;视频轮询 8s 首查/10s 步进/15min 上限;workbench 轮询 3s/5s/180 次;LLM timeout 240s;图片供应商 180s;模型测试 45s。
- 启动恢复四扫描:videos.processing→failed+退款、tasks.split_status=processing→Mock 回退、workbench_tasks.processing→按 video 状态收口、assets.queue_status=generating→回 queued。v1.3 P2 范围注记(W10 更新:四扫描已全部落地):Go P2 W6 已内联实现全部四处(退款走 F9 唯一口 ClaimVideoRefund 认领置位)+幂等可重复启动——原注记「workbench_tasks 随 M11」已过时。实现差异登记(v1.3 W10 入正文):①逐段容错——冻结 videos 段无 catch(异常即崩启动,SQLite 同步 API 副作用非设计)vs Go 统一逐段容错不挡启动;②workbench finished_at 冻结
datetime('now','localtime')vs GoNOW(3)(方言惯例);③恢复扫描用 SplitRecoverRow 窄投影(§5,W8 集成抽样抓获后修)。 - 跨聚合扣款时序(v1.3 定稿,取代 TECH.md §5 actor+outbox 草案原则):对齐冻结同步流——
handler 内 credits.TryDeduct(CAS)→ 业务落库(videos/images 行)→ providers 调用 → 失败 → credits.Refund(F9 唯一口);异步轮询 goroutine 持 ctx,重启丢轮询由启动恢复兜底(与冻结 setTimeout 语义等价)。不引 user actor 串行与 outbox 补偿:CAS 行锁已防超扣(冻结单进程无跨进程面);RunWithRetry/isConflict 无生产原型(qmtv/pms 双侧零调用)。internal/actor、internal/event(outbox relay)维持 P0 骨架预留不强接。 - Mock 保真:SVG 渐变图/动画 SVG 视频/手写 WAV(16kHz/16bit/mono,voice 基频 female 523/male 330/child 659/narrator 392Hz)/19 种风格缩略图幂等文件名——Go 版产物对前端不可区分。
- 种子:admin@qmtv.com/admin123456(10 万积分)、demo@qmtv.com/demo123456(1335 积分+示例任务)、distribution_settings(0.08/0.02/100)、2 公告;幂等(有 admin 即跳过)。
7. 前端项目与演进轨道(v1.1 立项)
裁决(2026-10-17):推翻「前端零改动、直接用 qmtv/ 冻结前端」口径——新建
frontend/独立前端项目。选型依据与优劣对比见prd/前端技术对比-qmtv-v1.md(§2.3 可迁移经验与 §5 钉子表继续有效)。
7.1 项目形态(初始 = 复制,行为对等)
| 项 | 内容 |
|---|---|
| 位置 | 仓库根 frontend/(与 backend/ 对称,非冻结区) |
| 原样复制 | src/(31 文件 9668 行:根 5 + components 2 + views 24)、index.html、vite.config.js(proxy /api /files → :3300 不变)——探查证实 src/ 零路径耦合(import 仅 vue/vue-router + 相对路径,api.js 纯同源 /api),复制即用 |
| 裁剪重写 | package.json:仅保留前端 4 依赖(vue、vue-router + dev 的 vite、@vitejs/plugin-vue;原 6 依赖中 better-sqlite3/express/multer/jszip 属后端,jszip 前端也未用);scripts 仅 dev/build/preview |
| 排除 | server/、data/、dist/、node_modules/、docs/、shots/、一次性脚本、package-lock.json(重装生成) |
7.2 演进三件(后续期渐进落地,吸收 pms 模式而非换框架)
| # | 项 | 治理什么 | 起步方式 |
|---|---|---|---|
| E1 | @tanstack/vue-query 服务端状态层 | 各页面自管 loading/缓存/失效的复杂度爆炸(Dashboard 1328 行单文件的根因);等价获得 pms React Query 191 处 useQuery 的声明式红利 | 从 Dashboard/Workbench 类多数据源页面开始接线 |
| E2 | vitest + @vue/test-utils | 零测试欠账:242 端点契约无前端侧断言 | 新代码带测试起步;参照 pms「页面测试近 1:1」与 docs/测试规划-qmtv.md 范式(全 mock,不连外部服务) |
| E3 | TypeScript 渐进类型化(✅ 二十五期 2026-10-19 落地:31 文件全量 TS 化,strict 闸门 ci 3.8) | 劣势 #1「无类型,接口契约只存在于约定」 | allowJs 混用,从 api.js/router.js 类型化起步,.js 按文件渐进改 .ts;纪律:api 返回类型显式收敛、禁 as any 兜底 |
7.3 什么是 TS(TypeScript)
TypeScript = JavaScript 超集 + 静态类型系统(微软 2012 年发布)。所有合法 JS 都是合法 TS;它在 JS 基础上加了「编译期类型检查」这一层:
- 错误发现时机前移:纯 JS 的类型错误运行时才爆(用户点按钮才发现
undefined);TS 在编辑器/编译期立刻红线。 - 运行时零开销:类型只是标注,编译(tsc / vite 内置 esbuild)时全部擦除,产出纯 JS——浏览器不认识也不需要认识 TS。
- 契约代码化:接口形状写成类型,传错即编译失败,无需等运行时验证。
- 渐进迁移:
allowJs允许 .js/.ts 混用,可按文件逐个改后缀,不必 big-bang——对 qmtv 前端 9668 行 JS 的存量是唯一现实路径。 - 对本项目的意义:PRD 里 242 端点的请求/响应契约目前只存在于本文档文字;E3 落地后变成代码里的编译期断言(api 函数返回类型显式收敛),契约漂移在 CI 就被拦下。
7.4 约束
- §2 全局契约 C1~C12 对
frontend/全部继续有效——前端演进的是内部形态,不改后端兼容面。 - 端口:dev :5173 与冻结版同端口(同一时刻只跑一个;需要并行时 vite 自动 +1);生产终态 Go :3300 托管
frontend/dist。 - 演进不反向解冻
qmtv/:冻结区保持对照基准原样。
1.3 - 01 · 系统设计方案
本文说明 QMTV 的产品定位、功能全景与各模块的设计方案。历史设计决策的完整推导过程见
D:\WORK\GAE\QMTV网站设计文档.md(22 节版本史)。
1. 产品定位
QMTV 是面向短剧/漫剧制作方的一站式 AI 内容生产平台,覆盖「剧本 → 资产 → 分镜 → 视频 → 成片 → 交付」完整链路,并配套内容生态(剧本库/评估/编剧激励)、企业协同(团队/权限/素材库)与商业闭环(积分/充值/返佣)三大体系。
设计原则:
- 生产工具 + 内容社区 + 商业化三合一:既做工具(对标巨日禄的项目内八类资源中心、工作台工具集),也做生态(对标新版 GAE 的剧本评估/剧本库/编剧广场),还做生意(积分计费/充值返佣/团队管理)。
- 积分是唯一硬通货:所有 AI 消耗统一折算积分,任务级流水可查,失败自动退款。商业模块(充值/返佣/提现)全部围绕积分展开。
- 功能零缺失复刻:以行业头部平台(manjuchuhai.cn 旧版/新版、video.jurilu.com)为参照逐项对齐,允许布局不同、不允许功能缺失。
2. 功能全景
3. 核心模块设计方案
3.1 剧本拆解(双引擎)
- 提交:
POST /api/tasks/v2接收剧本+配置(剧本类型/脸型/风格/时长区间/提取范围/自定义指令)。 - 引擎 A(规则拆解):
preprocess.js正则识别场次/分镜/角色,确定性输出,秒级完成,永远可用。 - 引擎 B(LLM 拆解):
tasks/v2走异步任务——取提示词中心生效的拆解指令(带输出合同:[CHARACTERS]/[SCENES]/[PROPS]/[SCRIPT]/[SHOT]/[IMG]/[VID]段落标记),llm.js调用注册表当前文本模型,parser.js状态机解析回填。失败自动回退引擎 A 并站内通知。 - 设计要点:输出合同用段落标记而非纯 JSON——大模型对分段文本的遵循度比严格 JSON 高得多;解析器按行状态机容错,允许模型输出多余空行/前后缀。
3.2 资产一致性与融合生图
- 角色/场景/物品在项目内统一建档(
assets表),生成图后image_id回填,形成"资产卡"。 - 提示词组装:
promptCenter.buildAssetPrompt(type, 描述, 脸型)拼接资产类别模板 + 占位符替换,保证同角色跨镜头提示词同源——这是一致性的第一保障。 - 融合生图(3 积分/张):组合角色/场景/物品卡描述 + 姿态/特效 + 画面描述 → 单张图,明确写入"保持所选资产外观一致性"指令。
- 图片改创:以项目内任意图为底图,原文提示词 +
[改创要求]后缀重新生成。 - 待生成队列:资产先入队(
queue_status: idle/queued/generating/done)不扣费,生成时才计费;失败回queued不丢队列;重启时recoverQueue()把 generating 回退 queued(持久化挂起)。
3.3 分镜与视频渲染
- 分镜支持 9 字段导演指令(环境状态/叙事任务/世界空间锁/位置锁/轴线锁/投影/群像锁/逐秒动作/转场点,JSON 存
director_shots.shot_fields)。 - 视频/图片生成统一走
providers.imageSmart / videoSmart(见架构文档 §5);批量渲染逐笔容错——单笔失败退款继续,返回 failed 计数。 - 智能审片(10 积分):五维评分(主体一致性/画面美感/动作连贯/字幕与口型/穿帮检测)+ 分级问题清单 + 修复建议;LLM 可用时走结构化 JSON 审片,否则规则引擎,
engine字段标注来源。经典工作台与导演工作台双入口,报告存review_reports。
3.4 剧本评估(多智能体流水线)
- 快速 100 / 深度 300 积分,提交冻结、完成结算、失败退回(
tryDeducttype=‘reserve’)。 - 8 角色 × 3 阶段:一级 交付规范主审+交叉核验 → 二级 全剧主审+前十精读+情绪人物+海外市场 → 三级 证据复核+评分审定。阶段落库
eval_projects.stage,前端轮询渲染流水线动画。 - 七维评分(合计 100):交付规范10 / 内容基础20 / 前10集转化20 / 人物情绪15 / 结构兑现15 / 海外适配10 / AI漫剧可视化10。
- 深度评估附逐集证据(按「第X集」切分,Mock 引擎与 LLM 提示词都要求按集给证据)。
3.5 剧本库市集与生态
script_library(官方库)与submissions(用户投稿)两源:官方库管理端维护;投稿过审后上架。字段完全市集化:题材/市场/受众/形式/标签/亮点/试读/人物小传/世界观/梗概/设定/集数/策划字数/编剧署名/评分/封面。- 详情目录自动从正文解析「第X集」生成;导入使用记
script_uses(近 7 天热度驱动排序与 🔥 角标)。 - 火花计划:
spark_joined报名送 100 积分;阶梯 = 星火(1-2部过审,100/部) → 炬火(3-5部,150/部) → 燎原(6+部,200/部);页面叠加现金赛事卡(奖池/奖项表/投稿入口)。
3.6 企业协同
- 组织/成员/邀请码;owner→admin→member 三级角色。
- 成员组(member_groups)与权限组(perm_groups:查看/编辑/删除/下载四权限 JSON)挂在 org_members 上;素材权限五项矩阵(查看/上传/下载/编辑/删除,org_members.material_perms)带联动规则:授上传/下载/编辑/删除自动含查看,授删除自动含编辑,取消查看清空全部——与行业对标产品一致。
- 数据看板:组织成员聚合的积分概览(可用+冻结)、近 14 天消耗趋势、项目消耗榜(credit_records.task_id 反查任务/项目名)、人员消耗榜。
- 素材库:
asset_folders目录树 +asset_library.folder_id归属 + 容量估算(图片/音频真实大小、缺失按 80KB/2MB 估算,配额 10GB)。
3.7 商业闭环
- 计费:
util.COSTS基础价 + 管理端计费配置(视频=拉取5+每秒5,图片10,2K预览15,拆解20,爆款5,评估100/300,审片10,转绘20/去字幕10/转高清15,融合3,一键转剧30/集,导出2/段)。计费参数存 system_settings,管理端可改。 - 充值:创建订单→上传凭证→管理审核→
approve入账 + 两级返佣(一级 8%、二级 2%,从 distribution_settings 读)→ 单笔满 980 元自动affiliate_enabled开通推广资格。 - 提现状态机:pending→reviewing→topay→paid→(用户确认)done;任意审核阶段可驳回;pending/reviewing 可用户撤回解锁收益。非法流转被 W_FLOW 矩阵拒绝。
- 安全:JWT 密钥持久化
data/.jwt_secret(支持 QMTV_JWT_SECRET 覆盖);登录 20 次/分/IP、注册 5 次/时/IP 限流;密码 hash 存储;管理端接口全部adminRequired。
4. 数据模型设计要点
49 张表见架构文档 §6。关键设计决策:
- 多态引用:
images.task_id既存拆解任务 id(t_)也存导演项目前缀 id(dir_);videos.shot_id配合shots._tbl约定(providers 内部判断 director_shots/shots)指向不同表——避免为两套工作台建两套生成表。 - 积分流水即账本:
credit_records(task_id)使任意消耗可追溯到项目,管理端审计、项目积分记录、企业看板共用这一张表。 - 轻量迁移:所有列变更走 migrations 数组幂等 ALTER,历史兼容靠 COALESCE/DEFAULT 占位值(注意:迁移 DEFAULT 值会挡住后续数据回填,需显式覆盖,见交接文档踩坑 §7.4)。
- 种子数据幂等:剧本库种子按标题匹配回填,且只覆盖"默认占位值"(market=‘中国内地’ 等),不覆盖人工修改。
5. AI 链路设计
- 模型接入注册表(model_endpoints):purpose ∈ text/image/video,每条独立 base_url/api_key/model/enabled,
active_model_<purpose>指定当前使用;管理端可测试连通。 - 提示词中心(prompt_templates):8 分类(split/shot_image/shot_video/asset_character/asset_scene/asset_prop/video_tag/viral+eval_quick/eval_deep)版本管理,单分类单生效;占位符
{描述}{脸型}{场次}{景别}{转场}{风格基调}生成时替换。 - 失败语义统一:providers 内部退款并抛
{status, message},全局错误中间件透传;调用方不得重复退款。
6. 视觉设计体系
- 三主题 CSS 变量切换(
html[data-theme]= dark/light/gae + localStorage),深色为主品牌(深空蓝 #04060e 底 + #2f6bff→#22d3ee 电光蓝渐变),玻璃拟态面板(backdrop-blur + 半透明边框光晕)。 - 登录页左右分屏品牌大图;分镜卡 v-tilt 自定义指令 3D 悬浮倾斜(触屏禁用)。
- 导航为分组下拉(工作台/创作工具/内容生态),菜单文字固定浅色(浅色主题下 var(–text) 会在深色菜单底上不可读——历史踩坑)。
- 移动端断点:1100px 导航换行、900px 网格降列。
7. 兼容对标对照
| 参照平台 | 已对齐模块 |
|---|---|
| 巨日禄 video.jurilu.com | 项目八类资源中心、融合生图、图片改创、待生成队列、公共池、参考视频/音频、本入剧出、剧本超市、团队管理(看板/组/权限/素材权限)、企业素材库(目录树/容量)、工作台工具集(转绘/去字幕/转高清——对方还标记"即将上线")、火花计划、作品展示 |
| 新版 GAE newgae.manjuchuhai.cn | 剧本评估中心(8角色3阶段七维)、剧本库市集(多维筛选/详情五区块/试读)、编剧广场、投稿中心、账号中心、全站搜索 |
| 旧版 GAE manjuchuhai.cn | 拆解台完整配置、视频页四一键、15 项功能更新全量、双工作台 |
1.4 - 前端技术对比:qmtv(Vue3)vs pms web(React18+AntD5)
文档定位:回答"前端不改写是确定的,但两套前端技术各有什么优劣"。结论前置:qmtv 前端零改动(已裁决);本文是选型对比存档 + 为 Go 移植钉住前端兼容面 + 沉淀"若未来前端演进,哪些 pms 经验值得吸收"。事实基线:2026-10-17 双仓库实测。
修订(2026-10-17,PRD v1.1):§3 结论「前端不动」已被新裁决取代——新建
frontend/独立前端项目(初始复制 qmtv 前端)+ 演进三件(@tanstack/vue-query / vitest+vue / TS 渐进),详见prd-qmtv-v1.md§7。本文 §1/§2 事实对照、§2.3 可迁移经验(vue-query 等价获得、测试文化、类型收敛纪律)与 §5 钉子表全部继续有效;原文保留存档不改。
1. 两套技术栈事实对照
| 维度 | qmtv(Vue3 单体) | pms web(React 中后台) |
|---|---|---|
| 框架/语言 | Vue 3.5 + vue-router 4,纯 JS 无 TS | React 18.3 + TS 5.6(strict:true,关 noUnusedLocals) |
| 构建 | Vite 8(vite.config 13 行,proxy /api /files) | Vite 5(tsc -b 前置 + vite build) |
| UI 库 | 无——全部手写组件 + 自定义指令(v-tilt 3D 倾斜)+ CSS 三主题(dark/light/gae) | AntD 5.22 + ProComponents(实际只用 ProLayout 布局壳,ProTable/ProForm 未用) |
| 请求层 | src/api.js 46 行:fetch 相对路径 /api、401 自动 refresh 重试一次、localStorage 存 token | axios + 拦截器;UnwrappedClient 双泛型修补运行时解包与 TS 签名的断裂 |
| 服务端状态 | 无——各页面自管 loading/缓存(Dashboard 1328 行单文件是代价) | TanStack Query 重度:191 useQuery / 86 useMutation / 363 queryKey + 自研 QueryTable 封装 |
| 客户端状态 | router.js 内轻量 reactive store(user/notifications) | Zustand 4 个极小 store(<100 行) |
| i18n | 无(纯中文) | react-i18next 四语 1750 key/语言 + coverage.test 双重防护(语言间 parity + t() 字面键扫描) |
| 测试 | 零(仅根目录 4 个 verify_*.js API 冒烟脚本) | vitest 142 文件 639 例(页面测试与源文件近 1:1)+ Playwright 双册(断言册 API-first 25 例与截图册物理隔离) |
| 路由 | 集中 24 条 + meta.admin 守卫 + 全局 beforeEach | 集中 110 Route、零懒加载、组件包裹式守卫 |
| 规模 | 24 页面 9082 行(最大 AdminView 1371 行/Dashboard 1328 行单文件) | 104 页面 + 20 封装组件 + 96 测试文件 |
| 实时 | 原生 EventSource(/api/events?token=)+ 每分钟轮询补偿 | SSE/轮询按需(生命周期时间线组件) |
2. 优劣分析
qmtv Vue3 的优势(在它的场景里)
- 极简依赖、零构建心智:6 个运行时依赖,13 行 vite 配置,单人可全栈维护;对 AI 辅助开发极友好(上下文小)。
- 前端对后端零耦合:相对路径 + 单一 fetch 封装,后端换语言零改动(本次移植的前提,已验证成立)。
- 产品形态匹配:C 端气质的创意工具(3D 分镜/主题切换/悬浮客服),手写组件的自由度比 AntD 中后台模板更贴合;AntD 反而会给它套上管理后台的壳。
- vue-router 的 meta 守卫与 SFC 单文件组织,样板代码比 React 集中路由少。
qmtv 的劣势(真实欠账)
- 无类型:9082 行 JS,接口契约只存在于约定;AdminView 14 个 Tab 1371 行单文件,回归靠人肉。
- 无服务端状态层:缓存/失效/重试每页手写——Dashboard/Workbench 类多数据源页面复杂度爆炸的根因。
- 零测试:239 个后端契约没有任何前端侧断言。
- 无 i18n:纯中文硬编码,产品出海需全量改造。
pms web 的优势(可迁移的经验)
- React Query 服务端状态模式是 pms 最值得移植的工程习惯——loading/缓存/失效全部声明式,191 处 useQuery 换来的是每个页面只剩"数据+渲染"。qmtv 若演进,@tanstack/vue-query 可等价获得,不必换 React。
- 测试文化:页面测试近 1:1、Playwright 断言册与截图册物理隔离、i18n coverage 双重防护——这些约定与框架无关,可原样移植到 vitest+vue。
- 类型收敛纪律:api 函数返回类型显式收敛、禁止
as any兜底(注释溯源)——qmtv 若加 TS,这条是第一规则。 - AntD5 中后台生态确实是复制式开发效率的根源;但注意 pms 实际只用了 ProLayout,ProTable/ProForm 红利并未吃到——“上了 AntD 就高效"要打折扣。
pms web 的劣势(对比中看到的税)
- axios 运行时解包与 TS 方法签名的断裂需要
UnwrappedClient类型修补层持续还债;Row 接口仍带[k:string]:any兜底。 - 110 条路由零懒加载,bundle 全量;qmtv 反而做了按页面 code-split。
- ProComponents 引入而未深度使用,依赖面大于收益面。
3. 结论
| 问题 | 结论 |
|---|---|
| 现在要不要动 qmtv 前端 | 不动。它与 Go 后端的耦合面只有 8 条契约(§5),Go 版逐条钉住即可;产品已建成,重写无动机 |
| 若未来前端演进,选什么 | 沿 Vue3 生态增量演进:加 TS(渐进)+ @tanstack/vue-query(服务端状态)+ vitest(补测试)——吸收 pms 的模式而非换框架 |
| 对 Go 移植的输入 | §5 的 8 条契约钉子进 PRD(已完成)与 TECH.md;SSE/token query/时间格式三处最易破 |
4. 迁移成本对照(假设反向迁移,仅作存档论证)
qmtv→pms 栈重写 ≈ 24 页面×(组件重写+React Query 接线+测试补齐) ≈ 与后端 Go 移植同量级的工程;而它解决的三个痛点(类型/服务端状态/测试)都能在 Vue 生态内补齐。重写框架 = 高成本解决低价值问题,此结论与"后端为何值得重写”(生产化运营的并发/部署/一致性硬需求)不同质。
5. Go 侧 8 条逐字节兼容约束(钉子表,PRD §2 已契约化)
| # | 契约 | 前端依赖点 | Go 版钉子 |
|---|---|---|---|
| 1 | 响应信封 {ok,data} / {error} | api.js:37 解包 .data、抛 {status,message} | gateway.OK/Err helper 全局唯一出口 |
| 2 | /api/health 裸 {ok:true,name:"QMTV"} | DEPLOY.md 监控/冒烟 | 不走信封 |
| 3 | token query 双通道 | EventSource/img 标签无法带 header | auth 中间件白名单 |
| 4 | SSE 帧格式+25s 心跳 | App.vue addEventListener 四类事件 | SSE hub 逐字节对齐 |
| 5 | uid 前缀+UUIDv4 格式 | slice 截取(routes2.js:368/enterprise.js:40 同语义前端存在) | infra/ids 前缀常量表 |
| 6 | scrypt N=16384/r=8/p=1 salt:hash | 登录面(种子账号兼容) | infra/crypto 参数显式 |
| 7 | 时间 YYYY-MM-DD HH:MM:SS 字符串 | 直接渲染不解析 | LocalTime 类型,禁 RFC3339 |
| 8 | 中文错误文案+错误码面 | 前端正则匹配(/所有者/、/密码/、429 文案) | PRD C9 冻结文案表 |
1.5 - 04 · 待开发功能与规划
本文供排期参考。分四类:A 真实模型接入(最高价值)、B 产品功能补强、C 生产化改造、D 技术债与优化。每项标注预估规模(S≤1天 / M≤3天 / L>3天)与前置依赖。
A. 真实 AI 能力接入(拿到 Key 后第一优先)
| # | 事项 | 规模 | 说明 |
|---|---|---|---|
| A1 | 文本模型接入验证 | S | 管理后台填 Key→测试连接→设为当前;验证 LLM 拆解/评估/审片三条链路自动升级(llm.isAvailable 分支已就绪) |
| A2 | 图片模型接入验证 | S | 验证 /images/generations 兼容性(比例→尺寸映射在 providers.generateImageReal);重点测 9:16 竖屏 |
| A3 | 视频模型接入验证 | M | 异步任务型接口已在 providers 适配(提交+轮询+下载),需用真实供应商联调:超时(15min)、同步 url 直返、错误码三类路径 |
| A4 | 智能审片 Vision 升级 | M | 当前审片文本摘要式;接入 Vision 模型(图片入参)做真正的画面级审片——reviewShots 已预留 engine 标注与回退 |
| A5 | 成本核账 | S | 真实接入后校准计费配置(管理后台计费设置),对照供应商定价调整 积分/张、积分/秒 |
验收清单(A1-A3 通用):拆解→出图→渲染→导出全链路真实素材走通;失败退款正确(故意填错 Key 验证);SSE 通知到达;项目积分记录与账实一致。
B. 产品功能补强(对标差距已清零后的深化)
| # | 事项 | 规模 | 说明 |
|---|---|---|---|
| B1 | 剧本超市独立市集页 | M | 当前投稿过审剧本混在 /library 市集与管理端;可拆独立「剧本超市」页(购买/订阅制、按播放分成是巨日禄未做完的方向) |
| B2 | 作品展示公开化 | S | /showcase 目前需登录;可开放匿名浏览 + 作者主页聚合其作品 |
| B3 | 素材权限矩阵落地执行 | M | 五项权限已存储+联动,但 asset_library 接口未按权限拦截(仅管理语义);需在共享素材读取/上传/删除处校验成员 material_perms |
| B4 | 成员独立配额 | M | organizations.credit_allocation_mode 已有 pooled/independent 字段但未实现 independent 分支(成员配额 quota 列闲置) |
| B5 | 手机号登录/找回 | M | 账号中心已留占位;需接短信服务商 + users.phone 列 + 登录分支 |
| B6 | 邮箱验证/找回密码 | M | 改邮箱已做(密码验证);找回密码需邮件服务 |
| B7 | 剧本库 tier 分级 | S | 新版 GAE 有 premium/free 分级;script_library 可加 tier 列 + 市集页筛选项 |
| B8 | 批量任务中心 | M | 全自动渲染/批量生成目前同步逐笔;量大后建议落任务队列(DB 表 + 轮询即可,不必上 MQ)+ 独立任务中心页 |
| B9 | 移动端适配 | M | 现有断点仅保证不烂;拆解台/视频页在小屏的体验需专项优化 |
| B10 | 运营位体系 | S | 首页 hero 可配置化(公告已有,扩为运营位:banner/活动入口),管理端配置 |
C. 生产化改造(上生产环境前必做)
| # | 事项 | 规模 | 说明 |
|---|---|---|---|
| C1 | 数据库迁移 SQLite→MySQL/PG | L | better-sqlite3 同步 API 遍布全栈;建议先做 SQL 方言审计(当前用大量 SQLite 特有 datetime(’now’,’localtime’) 与字符串拼接 IN),或短期维持 SQLite+定时备份(小团队可接受) |
| C2 | 对象存储 | M | data/files 本地盘 → OSS/S3(assets.js saveUpload 与 providers.finishVideo 两处写入点收敛良好,改造成本可控) |
| C3 | 进程守护与水平扩展 | M | pm2/容器 + 健康检查;SSE 与限流是进程内存态,多实例需改 Redis(bus 与 rateLimit 两处) |
| C4 | 日志与监控 | S | stdout → 文件轮转(如 pino);关键指标(生成成功率/退款率/供应商耗时)打点 |
| C5 | HTTPS 与域名 | S | 部署层(nginx/caddy)解决;注意 SSE 需要 proxy_buffering off |
| C6 | 支付对接 | L | 充值目前人工转账+凭证审核;接微信/支付宝需订单回调 + 幂等入账(recharge.js approve 逻辑可复用) |
| C7 | 安全加固 | M | 密码强度策略、接口幂等键、上传文件类型魔数校验、管理端操作审计日志(credit_records 已有雏形) |
| C8 | 备份自动化 | S | data/ 定时备份脚本 + 异地存储 |
D. 技术债与优化
| # | 事项 | 规模 | 说明 |
|---|---|---|---|
| D1 | index.js 拆分 | S | 824 行主入口可拆出 auth/tasks/videos/admin 四个模块,降低合并冲突 |
| D2 | 测试体系 | M | 现有 verify_*.js 是冒烟脚本;建议引入 vitest,把 verify 场景转成断言用例(后端纯函数/路由层) |
| D3 | 前端组件抽取 | M | 弹层/表格/标签在多页重复,可抽 BaseModal/BaseTable;三主题变量已就绪 |
| D4 | 旧表清理 | S | providers 表(旧供应商管理)已无路由引用,可归档;exchanges 兑换功能未接 UI |
| D5 | providers.js 真实供应商矩阵 | M | 当前视频适配只验证过一类异步接口;多供应商(字段名差异)可加 adapter 映射表 |
| D6 | 国际化 | L | 全部中文硬编码;海外市场卖点(评估七维含海外适配)落地前需 i18n |
建议节奏
- 第一周:A1+A2+A3(真实接入验证)+ C8 备份 —— 把"演示态"变"真实态"
- 第二周:A4 审片 Vision + A5 成本核账 + D1 拆 index.js
- 之后按业务方向二选一:To B 深耕 → B3/B4/C6(权限落地+独立配额+在线支付);To C 生态 → B1/B2/B7(市集与社区)
- 上生产前:C1-C5 按 C3(单机 pm2 + SQLite 维持)或 C1(多实例)两条路线二选一
已知问题(截至交接)
- 无 P0/P1 级 bug;以下为已记录的低优先观察项:
- 提交拆解无 Key 时同步返回(量大会占连接),有 Key 后自然变为异步
- 限流内存桶重启清零(C3 一并解决)
- 作品墙/共享项目列表分页上限 60/默认(量大需翻页)
- 移动端仅断点兜底(B9)
2 - 使用手册
- QMTV 使用说明书——页面图解版操作手册:24 页五段卡片(做什么/操作步骤/规则与注意)+ 计费速查表
- 人工测试清单——功能走查清单:T0~T7 旅程分层,覆盖登录、创作、生成、导出全流程
- README——手册目录与截图说明
2.1 - QMTV 使用说明书(页面图解版)
本手册面向 QMTV 平台的三类用户:工作台用户(拆解-出图-成片创作者)、企业管理员(组织与分销)、平台管理员(
/admin运营方)。 演示环境:http://localhost:3300(启动见 docs/DEPLOY.md §2.5);演示账号admin@qmtv.com / admin123456(管理员)、demo@qmtv.com / demo123456、producer@xingmang.cn / demo123456。 截图来自 Playwright 截图管线(frontend/e2e/snapshots.spec.ts,重跑cd frontend && npm run test:snapshots);Mock 模式下素材图为占位 SVG,真实供应商接入后为实际生成结果。 配套文档:人工测试清单(QA 走查底稿)· e2e 场景清单(自动化覆盖地图)。
测试旅程总览
flowchart LR
A[第1章 快速上手] --> B[第2章 创作主链]
B --> C[第3章 创作工具]
A --> D[第4章 内容生态]
A --> E[第5章 积分与账户]
D --> F[第6章 企业与分销]
G[第7章 管理后台]第1章 快速上手
1.1 登录(/login)
入口:未登录访问任意页面自动跳转;路由
/login。

做什么:登录 QMTV 账号,进入工作台。
操作步骤:
- 输入「邮箱」与「密码」(可点「显示」核对明文);
- 点「登 录」(请求中显示「登录中…」),成功后进入首页
/。
规则与注意:
- 登录失败为原生弹窗提示(如「邮箱或密码错误」),非 toast;
- 页面不显示演示账号——演示账号见本手册头部;
- 左侧为产品流程卡:「01 剧本拆解 → 02 智能出图 → 03 成片合成」。
1.2 注册(/register)
入口:登录页「还没有账号?立即注册」;路由
/register。

做什么:注册新账号,注册即赠体验积分。
操作步骤:
- 填「用户名」「邮箱」「密码(至少 8 位)」;
- 有企业邀请码则填「企业邀请码(可选,注册即加入企业)」,没有留空;
- 点「注册」,成功后自动登录进入首页。
规则与注意:
- 注册赠送 200 体验积分(台账「注册赠送 200 积分(初始)」,后台「计费配置」可调);
- 填了有效邀请码→邀请人得 50 积分;无效邀请码静默忽略;
- 昵称留空取邮箱 @ 前缀;密码后端强校验 ≥8 位。
1.3 申请企业入驻(/enterprise-apply)
入口:登录页「企业客户?申请企业入驻」;路由
/enterprise-apply,无需登录。

做什么:企业客户提交入驻申请,平台审核通过后自动创建企业组织并任命申请人为负责人。
操作步骤:
- 填「企业名称」「联系人」「申请说明」;
- 点「提交申请」→ 页面显示「申请已提交,等待审核。可在登录后于「企业管理」中查看进度。」。
规则与注意:审核动作在管理后台「企业审核」tab(§7.5);审核结果以站内通知告知。
1.4 首页(/)
入口:登录后默认落点;顶栏「首页」。

做什么:内容门户——查看积分余额、从四张入口卡进入核心功能。
操作步骤:整卡点击跳转:「01 剧本拆解台」→ /dashboard;「02 剧本视频生成」→ /video;「03 独立生成工具」→ /independent-tools;「04 爆款分析」→ /viral-analysis。
规则与注意:右上统计块实时显示「积分余额」与「当前账号」。
1.5 顶栏与全局功能
做什么:全局导航、搜索、主题、通知与账号入口。
操作步骤与规则:
- 导航下拉:「工作台」(拆解台/视频生成/导演工作台)、「创作工具」(独立生成/工作台工具集/本入剧出/作品展示/爆款分析)、「内容生态」(评估中心/剧本库/投稿中心/编剧广场/火花计划);「企业管理」「推广与收益」仅企业成员可见;「管理后台」仅管理员可见;
- 全站搜索(Ctrl/Cmd+K):搜「📚 剧本」与「✍️ 编剧」,回车跳首个结果;
- 主题切换:深色→浅色→雅致三态循环,记忆在浏览器本地;
- 工作台切换:「原始工作台」/「导演工作台」两版,切换即跳对应工作台;
- 🔔 通知:点开面板(60 秒轮询),支持单条/全部已读;业务完成另有实时推送 toast(视频/拆解/充值/工具四类);
- 积分胶囊:「昵称 · 可用 N 积分」,点击进账号中心;
- 右侧按钮:「充值」「积分记录」「修改密码」「退出」;
- 公告横幅:管理员发布的活跃公告显示在顶栏下,点「我知道了」不再显示。
第2章 创作主链(拆解 → 出图 → 成片 → 导出)
2.1 剧本拆解台(/dashboard)
入口:顶栏「工作台」→「🧩 剧本拆解台」;首页入口卡 01。

做什么:粘贴剧本文本,AI 拆解为场次/分镜/资产,再故事板抽卡生成分镜图——全链路的起点。
操作步骤:
- 左栏「任务配置」:上传
.txt或点「📚 剧本库导入」选现成剧本,或直接粘贴「剧本文本」(提示:「强烈建议一集一集放入剧本,拆解效果更佳」); - 填「任务名称」、选「剧本类型」(国内短剧/海外短剧)、「人物脸型风格」(亚洲中国/非亚洲)、缩略图墙选「视觉风格」(真人短剧/3D 动画/2D 插画三组共 19 种)、「成片时长(分钟)」范围与「特殊要求」;
- 点「立即合并放入后台拆分」→ toast「拆解完成:X 场 Y 镜,消耗 Z 积分」(异步路径为「已提交后台 AI 拆解」,完成后实时通知);
- 「智能资产提取」卡勾选「人物/场景/道具」范围点「开始提取」→「已提取 N 个资产图,消耗 N×单价 积分」;
- 右栏故事板:逐镜「📝 编辑提示词并生成」(10 积分/张)或用「流程控制」的「一键全自动串联生成」;「🔍 预览2K」可看 15 积分/次的高清预览;
- 「执行记录」中任务卡显示状态(完成/失败/处理中)与「N 镜头」,支持打开/复制/重命名/删除。
规则与注意(计费详见附录 A):
- AI 拆解按 token 计费:每积分 ≈ 1000 tokens(「文本拆解按 token 计费」提示行实时显示当前模型与换算);「🤖 AI 重新拆解」固定 20 积分(弹层确认「将清空现有分镜与资产并重新拆解」);
- 分镜图 10 积分/张、2K 预览 15 积分、资产提取按张数×10;
- 「图片规则说明」:自定义图需去独立生成工具页生成再上传;独立生成结果仅保存 72 小时;
- 「💰 项目积分」弹层可查本项目全部消耗明细;积分不足统一 toast「积分不足」。
2.2 剧本视频生成(/video)
入口:顶栏「工作台」→「🎬 剧本视频生成」;首页入口卡 02。

做什么:接续拆解产物,逐镜生成视频、串行打标、审片、配音,一键打包交付(图片包/视频包/剪映包)。
操作步骤:
- 顶部「选择任务」选拆解任务(选项格式「任务名(N镜)」);
- 逐镜填「视频提示词」(可粘贴「参考图槽位」的
@图片N引用)、「时长(秒) 1~15」「构图」(9:16/16:9/1:1)「分辨率」(1080p/720p),点生成; - 批量动作:「🏷️ 一键串行打标签」(确认「消耗 N×10 积分」)、「⚡ 一键全自动串行渲染全案视频」(确认「估算消耗 X 积分(拉取5 + 每秒5×时长)」)、「🕵️ 智能审片」(确认「消耗 10 积分」→ 报告含综合分与五维:主体一致性/画面美感/动作连贯/字幕与口型/穿帮检测);
- 「🎙 配音」:选音色(旁白解说/女声/男声/童声)生成旁白(免费);
- 交付:「📥 一键下载图片+提示词」「📦 打包下载已完成视频」「✂️ 剪映导入包」(解压到剪映草稿目录即可打开)、「进入导出」→ 导出成片页。
规则与注意:
- 视频计费 = 拉取 5 + 每秒 5×时长(「计费说明」行实时显示当前单价);打标 10/镜;审片 10/次;配音免费;
- 「取消生成」提示「供应商已产生费用,取消不退积分」;
- 左栏可上传音频素材(「音频将自动进入剪映导入包与成片混音流程」);
- 生成完成/失败有实时 toast 推送,失败自动退积分。
2.3 导出成片(/export/:taskId)
入口:视频页工具条「进入导出」;路由
/export/:taskId(无顶栏入口)。

做什么:对已渲染分镜发起成片导出编排,显示进度与段数。
操作步骤:进入页面自动执行——已有导出则显示进度,否则自动新建并 toast「已开始导出,共 N 段」;2 秒轮询至终态 toast「导出完成」/「导出失败」。
规则与注意:每段消耗 2 积分;本页无 ZIP 下载按钮——产物下载在视频页的三个打包按钮;失败且积分不足显示「积分不足,无法导出」。
第3章 创作工具
3.1 独立生成工具(/independent-tools)
入口:顶栏「创作工具」→「🖼 独立生成工具」。

做什么:不依赖剧本项目,独立生成图片/视频素材,或一键生成解说成片。
操作步骤:
- 「图片生成」:填提示词、选比例(9:16/16:9/1:1)、可加最多 9 张参考图,点「生成图片(10 积分)」;
- 「视频生成」:填提示词选比例,点「生成视频(50 积分)」;
- 「🎬 一键成片」:填标题与解说剧本(≥50 字),点「开始生成」→「成片完成:N 个解说段落」;
- 「生成记录」中可下载原图/重用提示词/删除(删除确认「取消已开跑任务不退积分」)。
规则与注意:顶部规则「结果仅保存 72 小时,请及时下载;参考图最多 9 张」;一键成片 30 积分;视频时长滑杆仅展示不参与计费(扁价 50)。
3.2 本入剧出(/oneclick)
入口:顶栏「创作工具」→「📖 本入剧出」。

做什么:粘贴小说或剧本,自动识别分集、生成分镜并逐集渲染成片。
操作步骤:
- 「+ 新建转剧项目」:填项目名称、粘贴文本(支持「第X集」划分,无集名按段落切分)→「已识别 N 集」;
- 项目卡上「🚀 一键转剧」→ 确认「将自动生成分镜并逐镜渲染视频(每集 30 积分)」→ 逐集进度,结束「转剧完成」(单集失败自动退款继续其余)。
规则与注意:每集 30 积分扁价;删除项目会「分集与成片素材一并删除」。
3.3 工作台工具集(/workbench)
入口:顶栏「创作工具」→「🛠 工作台工具集」。

做什么:成片后处理三件套——转绘/去字幕/转高清。
操作步骤:
- 选工具卡:🎨「一键转绘」(20 积分/次,填风格描述如「赛博朋克霓虹风」)、🧽「去字幕」(10 积分/次)、💎「转高清」(15 积分/次,选目标 2K/4K);
- 「上传视频(mp4/webm/mov)」或「引用已有成片」下拉选择;
- 点「开始{工具名}(N 积分)」→ 完成 toast「{工具名}完成」。
规则与注意:「生成失败自动全额退回积分,可放心提交」;转绘未填描述确认「将使用默认「电影级写实质感」」。
3.4 导演工作台(/director3d)
入口:顶栏「工作台」→「🎯 导演工作台」(或顶栏工作台版本切换)。

做什么:项目制三级工作台——剧本自动分集 → 资产提取 → 9 字段导演指令分镜 → 逐镜出图出片 → 导出交付包。
操作步骤:
- L1 项目列表:「+ 新建项目」(名称/脸型/视觉风格/剧本文本)→「项目已创建,识别出 N 集」;支持「📥 导入项目」(.zip)与「导出 ZIP」;
- L2 项目详情(八 tab):「🎞 片段管理」进分集工作台;「👤 角色 / 📍 场景 / 🎒 物品」资产三 tab 可「加入待生成队列」(不扣费)再「⚡ 批量生成」(按张×10,失败部分自动退款)或「📦 公共池导入」;「🖼 融合生图」选角色/场景/物品+画面描述生成一致性图(3 积分);「✨ 图片改创」以已有图为底图改创(10 积分);「🎬 参考视频」「🎵 参考音频」上传素材;项目头可「🏢 共享到企业」「🕵️ 智能审片(10 积分)」;
- L3 分集工作台:勾选范围「🎭 提取资产」;「🎬 生成分镜」(确认「按场次生成 3~6 镜/场」);每镜卡内编辑「导演指令(9 字段)」(格式「每行一条 键:值」)、「图片提示词」「视频参数」「视频提示词」,逐镜「🖼 生成图片」「🎥 生成视频」。
规则与注意:分镜/资产图按图片单价(默认 10);融合 3 积分;「待生成资产不会消耗积分,生成时才扣」。
第4章 内容生态
4.1 剧本库(/library)
入口:顶栏「内容生态」→「📚 剧本库」。

做什么:官方剧本市集——多维筛选找剧本,看详情(项目亮点/前三集试读/人物小传/世界观/故事目录),一键导入拆解台开拍。
操作步骤:
- 筛选:题材 chip(带 🔥 近 7 天热度)、「全部市场/受众/形式」下拉、搜索框(防抖)、排序(近期火爆/评分最高/最新上架);
- 点卡片开详情弹层,按锚点浏览各区块;点「📥 导入到拆解台使用」→ 自动跳拆解台并载入剧本。
规则与注意:浏览与导入免费(导入记录使用热度);完整剧本在导入后才载入拆解台。
4.2 投稿中心(/studio)
入口:顶栏「内容生态」→「📮 投稿中心」。

做什么:提交原创剧本投稿(过审上架剧本库),维护编剧档案。
操作步骤:
- 「+ 新建剧本投稿」:填「标题*」「分类」「目标市场」「制作形式(短剧/网剧)」「目标受众」「简介」「剧本内容*」;
- 「存草稿」或「提交审核」→「已提交审核,审核结果将在通知中告知」;
- 状态流转:草稿→待审核→已发布/需修改/已驳回;「需修改」可「查看意见」后「重新编辑提交」;
- 「编剧档案」:填笔名*/擅长题材/简介/代表作创建资料,资料展示在编剧广场。
规则与注意:投稿免费;过审后上架剧本库(§4.1)供全网使用;审核动作在管理后台「投稿审核」tab(§7.12)。
4.3 编剧广场(/writers)
入口:顶栏「内容生态」→「✍️ 编剧广场」。

做什么:发现编剧——按题材/排序浏览,看主页(入驻时间/在库剧本/代表作)。
操作步骤:搜索(回车)或点题材 chip;点卡片「查看主页」;主页内点在库剧本跳剧本库详情。
规则与注意:展示数据来源即投稿中心的「编剧档案」。
4.4 作品展示(/showcase)
入口:顶栏「创作工具」→「🎬 作品展示」。

做什么:平台成片作品墙——发布已完成成片让全平台看见。
操作步骤:「📤 发布作品」→ 选成片(仅已完成)+ 填「作品名称」→「发布到作品墙」;自己的卡片可「撤下」。
规则与注意:「发布不消耗积分」;发布后全平台可见。
4.5 火花计划(/spark)
入口:顶栏「内容生态」→「🔥 火花计划」。

做什么:创作者激励计划——加入即奖积分,投稿过审按阶梯持续奖励。
操作步骤:
- 点「🔥 立即加入(赠 100 积分)」→ toast「已加入火花计划,100 积分已到账」,数据卡开始实时展示投稿量/过审率/累计激励;
- 按本期赛事卡指引「立即投稿」(经投稿中心提交,过审自动进入评选);
- 阶梯:🪵 星火(过审 1-2 部,100 积分/部)→ 🔥 炬火(3-5 部,150 积分/部+首页推荐位)→ 🏆 燎原(6+ 部,200 积分/部+专属流量扶持+实物周边)。
规则与注意:重复加入提示「您已加入火花计划」;「怎么玩」三步:写剧本并投稿→过审即发布→按阶梯拿激励;FAQ 见页面底部四条。
4.6 爆款分析(/viral-analysis)
入口:顶栏「创作工具」→「📈 爆款分析」。

做什么:粘贴爆款链接或文案,AI 从四维打分并给改写钩子。
操作步骤:填「任务名(可选)」与「链接或文案」→「开始分析(5 积分)」→ 结果卡显示环形总分(60-94)+ 四维条(钩子强度/情绪共鸣/节奏密度/转化引导)+ 改写钩子(可复制);历史记录可回看。
规则与注意:每次 5 积分;空内容提示「请输入待分析内容」。
4.7 剧本评估中心(/evaluation)
入口:顶栏「内容生态」→「🔬 剧本评估中心」。

做什么:七维评分体系的剧本评估,快速(100 积分)/深度(300 积分)两档,产出结构化评估报告。
操作步骤:
- 「新建评估」:填项目名(选填)、选「评估方式」卡片(⚡ 快速—评分/结论/最多 3 项风险;🔬 深度—完整报告含逐集证据与海外适配)、可选填题材/市场/形式三下拉;
- 粘贴剧本文本(≥100 字,建议含人物小传/大纲/分集正文)或上传
.txt/.md/.docx(≤10MB); - 点「开始评估」→ toast「已提交,冻结 N 积分,完成后结算(失败自动退回)」;
- 流水线三阶段八角色实时推进(交付规范审计→全剧主审/前 10 集精读→证据复核/首席评分),3 秒轮询;
- 完成后查看报告:总分/等级/七维评分(交付规范 10+内容基础 20+前 10 集转化 20+人物情绪 15+结构兑现 15+海外适配 10+AI 漫剧可视化 10)/故事亮点/逐集证据/重点风险/推进建议,可「📥 下载报告」(.txt)。
规则与注意:提交冻结、完成结算、失败全额退回(「评估失败,冻结积分已自动退回」);字数不足提示「剧本内容至少 100 字」。
第5章 积分与账户
5.1 个人积分流水(/credits)
入口:账号中心「积分明细页」;顶栏「积分记录」为弹层版。

做什么:查看积分充值/消费/余额明细。
操作步骤:四格汇总(可用积分/累计增加/累计消费/当前余额)+ 流水表(时间/类型「增加|扣费」/事项/积分「+N|-N」/余额);「上一页/下一页」翻页(每页 20 条)。
规则与注意:「累计增加/累计消费/当前余额」三格基于当前页聚合,非全量统计。
5.2 充值(/recharge)
入口:顶栏「充值」;账号中心「充值」。

做什么:扫码转账充值积分,上传凭证待审核到账。
操作步骤:
- 左栏看「充值金额(元)」输入(≥1),页面实时预览「按当前汇率 1 元 = N 积分,预计获得 N 积分」;
- 按收款码转账(备注订单号)→「创建充值订单」→ toast「充值订单 xxx 已创建(N 积分),请转账后上传支付凭证」;
- 「我的订单」中对该单「上传凭证」(图片)→ 状态「待审核」;
- 管理员审核通过 → 状态「已到账」+ 实时 toast「💰 充值已到账」。
规则与注意:
- 汇率默认 1 元 = 10 积分(后台可调);无收款码时提示线下联系管理员;
- 「单笔充值满 980 元自动开通推广资格」(可在后台调整);
- 到账同时自动结算两级返佣(§6.2)。
5.3 账号中心(/account)
入口:顶栏积分胶囊点击。

做什么:个人资料/登录设置/积分/通知集中管理。
操作步骤:
- 修改密码:填原密码+新密码(≥8 位)+确认 →「密码已修改」;
- 修改登录邮箱:填新邮箱+当前密码 → 成功后强制登出跳登录页;
- 查看邀请码/注册时间/推广资格 tag/通知(最近 4 条)。
规则与注意:改密成功后端撤销全部登录状态(refreshToken 全失效),其他已登录设备需重登;手机号绑定暂未接入。
第6章 企业与分销(企业成员可见)
6.1 企业管理(/organization)
入口:顶栏「企业管理」(有 org_id 时显示;未加入企业时此页提供「提交入驻申请」与「使用邀请码加入」两个入口)。

做什么:企业组织管理——数据看板/成员角色/分组权限/素材权限/企业素材库/共享项目/积分流水。
操作步骤(已加入企业,7 个 tab 全员可见,管理动作按角色收敛):
- 📊 数据看板:积分余额(可用/冻结)/累计充值/累计消耗/近 14 天趋势与项目、人员消耗榜;
- 👥 成员与角色:所有者可「生成邀请码」邀新(对方邮箱→邀请码→对方在「使用邀请码加入」处兑换)、调整角色(成员/管理员)、移除;
- 🧩 成员组·权限组:所有者创建成员组(指定组长)与权限组(查看/上传/下载/编辑/删除组合);
- 🔐 素材权限:五项独立权限勾选(「授予下载/编辑/删除会自动授予查看;授予删除自动含编辑;取消查看会取消其余全部权限」);
- 🗄 企业素材库:目录树/类型筛选/文件夹管理;素材来自成员在拆解台「资产库」的企业共享;
- 🎬 共享项目:导演工作台开了「共享到企业」的项目在此汇集,可跳转查看;
- 💰 积分流水:组织维度台账。
规则与注意:角色三层——所有者(全部管理)/管理员(素材权限+看板+共享项目)/成员(按权限组访问)。
6.2 推广与收益(/distribution)
入口:顶栏「推广与收益」(企业成员可见;推广资格满 980 元充值自动开通)。

做什么:分销推广——分享推广码拿两级返佣,收益可提现或 1:1 兑换生产积分。
操作步骤:
- 概览卡复制「我的推广码」分享;好友注册时填入即绑定为下线;
- 下线充值即按比例返佣(到账自动结算):一级 8% / 二级 2%(后台可调);
- 「提现」tab:收益满 100 积分申请提现 → 状态机 待审核→审核中→待打款→已打款(平台打款后)→ 你点「确认到账」→已完成;待审核/审核中可「撤回」;
- 「兑换」tab:收益 1:1 兑换生产积分(最少 10)。
规则与注意:提现最低 100 积分(「最低提现 N 积分」);可提额度=累计佣金−全部提现申请(已驳回/撤回不释放额度);兑换与提现额度相互独立。
第7章 管理后台(/admin,管理员)
入口:顶栏「管理后台」(仅 admin);非管理员访问被顶回首页。左侧 tab 栏共 14 个 tab,30s 自动刷新概览。

做什么:平台运营全功能——用户/积分/内容/审核/计费/模型/提示词。
各 tab 速览:
| # | Tab | 做什么(关键动作) |
|---|---|---|
| ① | 概览 | 六格统计(用户/任务/图片/视频/发放/消耗)+「运行中任务」(视频生成中/拆解中)实时卡 |
| ② | 用户管理 | 搜索;每行「调整积分」(prompt 正负数+confirm)/「停用·启用」/「重置密码」(prompt ≥8 位)/「设管理员」 |
| ③ | 积分发放 | 选用户+金额+说明「发放」(confirm);「📥 导出积分流水(Excel)」(.csv) |
| ④ | 公告管理 | 「发布公告」(标题+内容)→ 顶栏横幅展示;可停用/启用 |
| ⑤ | 企业审核 | 入驻申请「通过」(自动建组织+任命负责人)/「拒绝」 |
| ⑥ | 提现审核 | 状态机推进:转入审核中→批准打款→(用户确认);「驳回」填原因 |
| ⑦ | 模型配置 | 三用途(文本/图片/视频)各「+ 添加接入」(OpenAI 兼容:显示名+接口地址+API Key+模型名)→「测试连接」「设为当前使用」;未配置自动走 Mock |
| ⑧ | 计费配置 | 五项:注册试用积分/文本每积分 Tokens/图片单价/视频拉取费/视频每秒积分 |
| ⑨ | 指令版本 | 拆解系统指令多版本管理:「设为生效」「下一次拆解即使用该指令」 |
| ⑩ | 提示词管理 | 各分类(拆解/出图/视频等)自定义提示词版本:编辑/上传 .txt/.md/设为生效/还原内置 |
| ⑪ | 充值审核 | 「收款码设置」上传;订单「通过」(confirm→「已到账 N 积分,返佣已结算」)/「拒绝」(填原因,默认「凭证有误」) |
| ⑫ | 投稿审核 | 「查看全文」「发布到剧本库」「需修改」(填意见)/「驳回」(填原因) |
| ⑬ | 任务审计 | 视频失败记录(「手动退款」confirm)/拆解失败记录(「重试拆解」) |
| ⑭ | 剧本管理 | 批量上传 .txt(文件名即标题)/手动新建全字段/评分(0-5)/封面/上架·下架/删除 |
规则与注意:
- 「模型配置」是全站 AI 能力开关:接入真实供应商后自动切换,失败自动回退 Mock;配置零环境变量;
- 「计费配置」改动全站即时生效(对照附录 A 各「可调」项);
- 充值「通过」即触发到账+两级返佣联动,不可逆。
附录 A 计费速查表
单价经源码核对(backend/internal/domain/credits/billing.go 及各域);标「可调」者经管理后台「计费配置」或 settings 即时生效。
| 项目 | 单价 | 可调 |
|---|---|---|
| 注册赠送 | +200 积分 | ✔(trial_credits) |
| AI 拆解(v2) | 按 token:每积分 ≈ 1000 tokens(最少 1 积分) | ✔(text_tokens_per_credit) |
| AI 重新拆解(旧版) | 20/次 | — |
| 分镜图/资产图/图片改创/独立图片 | 10/张 | ✔(image_generation) |
| 2K 预览 | 15/次 | — |
| 分镜视频 | 拉取 5 + 每秒 5×时长 | ✔(video_pull/video_per_second) |
| 独立视频 | 50/条(扁价) | — |
| 一键成片(解说) | 30/次 | — |
| 一键转剧 | 30/集 | — |
| 工作台工具 | 转绘 20 / 去字幕 10 / 转高清 15 | — |
| 融合生图 | 3/张 | — |
| 资产提取 | 10×资产数 | ✔(随图片单价) |
| 打标(单镜/一键) | 10/镜 | — |
| 智能审片 | 10/次 | — |
| 导出成片 | 2/段 | — |
| 配音 | 免费 | — |
| 爆款分析 | 5/次 | — |
| 剧本评估 | 快速 100 / 深度 300(冻结-结算制,失败退) | ✔(eval_cost_quick/deep) |
| 火花计划 | 加入 +100;过审阶梯 100/150/200 | — |
| 充值汇率 | 1 元 = 10 积分 | ✔(credit_per_cny) |
| 分销返佣 | 一级 8% / 二级 2%(到账自动结算) | ✔(commission_rate/level2_rate) |
| 分销开通 | 单笔充值满 980 元自动开通 | ✔(affiliate_threshold_cny) |
| 提现/兑换 | 提现满 100 起提;兑换 1:1 满 10 起兑 | 提现✔(min_withdraw) |
| 通用保障 | 生成失败自动退积分(工作台工具/评估/转剧/分镜视频等) | — |
附录 B 常见问题
- 登录掉线/全员掉线:JWT 密钥文件(
data/.jwt_secret)丢失或更换所致——迁移必须随data/整目录带走(见 DEPLOY.md §5.1); - 页面白屏:先查前端 dist 是否构建成功(:3300 托管的就是 dist/),再清浏览器缓存;
- 生成一直排队:未配置模型接入时全站走内置 Mock(秒级同步完成);若配置了真实供应商,排队属正常,完成/失败均有实时 toast;
- 生成失败积分去哪了:自动原路退回(台账可见「+N」退回行),工作台工具/评估/转剧等失败面全覆盖;
- 独立生成的图片不见了:独立生成结果仅保存 72 小时,请及时下载;
- 改密后被登出:改密撤销全部登录状态(安全设计),用新密码重登即可;
- 充值为 0 积分:订单需管理员审核到账,状态在「我的订单」可见;到账后有实时通知;
- 提现被驳回:看提现记录「备注」列的管理员原因;驳回/撤回的额度立即解锁可重新申请。
2.2 - 人工测试清单(QMTV QA 走查底稿)
自动化升格映射(场景 → E2E/人工三向归类)见 docs/test/e2e-qmtv-场景清单.md;操作说明见 QMTV使用说明书_中文.md。
用法:按 T0→T7 顺序执行,每步
☐打勾,实际:____填真实观察值/截图编号。失败即记 bug 编号。 测试数据:seed 三账号admin@qmtv.com / admin123456(管理员,10 万积分)、demo@qmtv.com / demo123456(1335 积分,seed 含欢迎通知+admin→demo 分销关系)、producer@xingmang.cn / demo123456(制片视角);计费走内置 Mock(未配置 model_endpoints 时全站同步 Mock,生成图/视频为占位素材)。 前置:环境已起——生产形态bash scripts/dev-up.sh(:3300)或 e2e 形态bash scripts/dev/e2e/e2e-up.sh(:3301,独立库,推荐——每次 up 全量重建,水位确定);登录 admin。 图例:🟢E2E-XX-NN=该用例主链路已被 Playwright 断言用例覆盖(frontend/e2e/assertion/);🟡待实现·E2E-XX-NN=场景已登记场景清单、用例待写;无标=纯人工。 ⚠️ E2E 只断 API 链路与关键业务结果(toast 存在/台账行/状态翻转):页面交互流、视觉布局、文案细节、跨页联动仍需按下表人工执行。
测试旅程总览
flowchart LR
A[T0 种子与环境] --> B[T1 创作主链闭环]
B --> C[T2 创作工具]
A --> D[T3 内容生态]
B --> E[T4 积分与充值]
D --> F[T5 企业与分销]
A --> G[T6 管理后台]
A --> H[T7 权限与边界]T0 · 种子与环境确认
目的:确认环境就绪、种子账号/数据在位、计费走 Mock 同步面。对照 e2e-verify 六项自检。
| # | 动作 | 预期 | ☐ | 实际 |
|---|---|---|---|---|
| T0.1 | curl http://localhost:3300/api/health | {"ok":true,"name":"QMTV",...}(e2e 环境换 :3301) | ☐ | |
| T0.2 | 三账号依次登录 | admin/demo/producer 均登录成功进首页 | ☐ | |
| T0.3 | admin 进 /admin 概览 | 用户数≥3;六格统计非空;30s 自动刷新 | ☐ | |
| T0.4 | demo 登录看积分胶囊 | 「demo · 可用 1335 积分」(e2e 库新注册则为 200) | ☐ | |
| T0.5 | admin /admin→模型配置 | 无接入(或全停用)状态=全站 Mock;有接入则确认测试连接 | ☐ | |
| T0.6 | 首页/Hero 与四入口卡渲染 | 拆解台/视频生成/独立生成/爆款分析四卡可点 | ☐ | |
| T0.7 | 顶栏横幅与铃铛 | 有活跃公告则显示横幅(「我知道了」可关);铃铛面板开合正常 | ☐ | |
| T0.8 | 主题三态切换 | 深色→浅色→雅致循环,刷新后记忆保持 | ☐ |
T1 · 创作主链闭环(拆解 → 出图 → 成片 → 导出)
核心旅程:一笔剧本从拆解到导出全通,扣费流水逐笔可对。建议用 demo 账号(低水位,扣费可见)。
| # | 动作 | 预期 | ☐ | 实际 |
|---|---|---|---|---|
| T1.1 🟢E2E-DASH-01 | /dashboard 粘贴一集剧本(≥百字)填名称选风格,点「立即合并放入后台拆分」 | toast「拆解完成:X 场 Y 镜,消耗 Z 积分」;执行记录新任务卡出现且镜数 >0;状态「处理中」→「完成」 | ☐ | |
| T1.2 | 打开任务看右栏故事板 | 按镜头组分镜卡;卡上有 #序号/场景名/状态 tag(待生成) | ☐ | |
| T1.3 | 「智能资产提取」勾人物+场景,「开始提取」 | toast「已提取 N 个资产图,消耗 N×10 积分」;资产三组卡出现(人物与四视图/场景俯拍/核心道具) | ☐ | |
| T1.4 | 任一分镜「📝 编辑提示词并生成」 | toast「生成成功」;分镜卡出现故事板图,状态 tag「已出图」 | ☐ | |
| T1.5 | 「流程控制」→「一键全自动串联生成」 | confirm 后批量出图,toast「已生成 N 张分镜图」 | ☐ | |
| T1.6 🔍 | 分镜卡「🔍 预览2K」 | 弹层出 2K 图,「下载原图」可用;流水新增 -15 | ☐ | |
| T1.7 | /video 选该任务,单镜生成视频(时长 5s) | toast「生成完成 5 秒 · 30 积分 · 模型 …」(拉取 5+每秒 5×5);分镜状态「已出视频」 | ☐ | |
| T1.8 | 「🏷️ 一键串行打标签」 | confirm「消耗 N×10 积分」→ toast「已打标 N 镜」;提示词尾追加「[一致性标签·自动] …」 | ☐ | |
| T1.9 | 「🕵️ 智能审片」 | confirm「消耗 10 积分」→ toast「审片完成:综合 N 分,M 个问题」;报告五维+问题清单 | ☐ | |
| T1.10 | 「🎙 配音」(已出视频的分镜) | 选音色生成,toast「配音已生成」;不扣费;历史配音列表在 | ☐ | |
| T1.11 | 「⚡ 一键全自动串行渲染全案视频」 | confirm「估算消耗 X 积分(拉取5 + 每秒5×时长)」→ toast「已渲染 N 段,消耗 C 积分」 | ☐ | |
| T1.12 | 三个打包:「📥 图片+提示词」「📦 视频」「✂️ 剪映导入包」 | qmtv-assets.zip / qmtv-videos.zip / qmtv-jianying.zip 三包下载,内容非空 | ☐ | |
| T1.13 🟡待实现·E2E-EXP-01 | 「进入导出」→ /export/:taskId | toast「已开始导出,共 N 段」;2s 轮询至「导出完成」;流水新增「导出成片 N 段(-2N)」 | ☐ | |
| T1.14 🟢E2E-CRD-01 | /credits 看流水 | 全程每笔扣费有对应「扣费」行(拆解/资产/分镜图/视频/打标/审片/导出);类型列「增加/扣费」分色 | ☐ | |
| T1.15 | 「💰 项目积分」弹层对账 | 合计消耗=各分项之和;台账列「时间/事项/积分/余额」 | ☐ |
T2 · 创作工具
| # | 动作 | 预期 | ☐ | 实际 |
|---|---|---|---|---|
| T2.1 🟢E2E-GEN-01 | /independent-tools 图片 tab:填提示词生成 | 「生成图片(10 积分)」→ toast「已提交生成任务」;.img-card 即现含提交提示词 | ☐ | |
| T2.2 | 参考 9 张上限 | 加第 10 张参考图 toast「参考图最多 9 张」 | ☐ | |
| T2.3 | 图片记录「下载原图」「使用此提示词」 | 下载 SVG 可开;提示词回填表单;删除有 confirm | ☐ | |
| T2.4 | 视频 tab 生成 | 「生成视频(50 积分)」→ 记录出「已完成」卡可播放 | ☐ | |
| T2.5 | 🎬 一键成片(解说剧本 ≥50 字) | toast「成片完成:N 个解说段落」;段落表(段落/解说词/画面)+下载;流水 -30 | ☐ | |
| T2.6 | 一键成片 <50 字 | 400 拦截「剧本内容过短」类提示,不扣费 | ☐ | |
| T2.7 🟢E2E-GEN-02 | /workbench 去字幕(上传小视频或引用成片) | toast「去字幕完成」;任务记录行状态「已完成」;流水 -10 | ☐ | |
| T2.8 | /workbench 转绘不填风格描述 | confirm「将使用默认「电影级写实质感」」;填描述则直接提交(20 积分) | ☐ | |
| T2.9 | /workbench 转高清选 4K | toast「转高清完成」;流水 -15 | ☐ | |
| T2.10 | /oneclick 新建转剧项目(含「第X集」标记文本) | 「创建项目」→ toast「已识别 N 集」(集数正确) | ☐ | |
| T2.11 | 「🚀 一键转剧」 | confirm「每集 30 积分」→ 逐集进度 →「转剧完成:N 集 N 段」;流水 N×30 | ☐ | |
| T2.12 | /director3d L1 新建项目 | 「项目已创建,识别出 N 集」;项目卡可进/导出 ZIP/删除(删除 confirm 全链删除) | ☐ | |
| T2.13 | L2 资产 tab:新建角色→加入待生成→批量生成 | 「已加入待生成队列」(不扣费)→ confirm「N×10」→「已生成 N 个,失败 N 个(失败已退款)」 | ☐ | |
| T2.14 | L2 融合生图(选角色+场景+描述) | 「融合图已生成」;流水 -3 | ☐ | |
| T2.15 | L2 图片改创(选底图+说明) | 「改创完成」;流水 -10 | ☐ | |
| T2.16 | L3 生成分镜→逐镜出图出片 | confirm「3~6 镜/场」→「分镜已生成(N 镜)」;9 字段导演指令可编辑保存 | ☐ | |
| T2.17 🟡待实现·E2E-DIR-01 | L3 全链(提取资产→生成分镜→出图→出视频) | director_shots 回填;视频卡片可播;各步流水与附录 A 单价一致 | ☐ |
T3 · 内容生态
| # | 动作 | 预期 | ☐ | 实际 |
|---|---|---|---|---|
| T3.1 | /library 筛选(题材/市场/受众/形式)+搜索+排序 | 列表联动;卡片显示「⭐ 评分 / 🔥 近7天 N 次 / N 集·N 字」 | ☐ | |
| T3.2 | 打开剧本详情弹层 | 锚点区块(项目亮点/前三集试读/世界观/人物小传/故事目录)按内容显隐 | ☐ | |
| T3.3 | 「📥 导入到拆解台使用」 | 跳 /dashboard 且剧本文本已载入(不扣费);详情计数+1 | ☐ | |
| T3.4 | /studio 新建投稿(全字段)→「存草稿」 | 「草稿已保存」;我的投稿行状态「草稿」 | ☐ | |
| T3.5 | 草稿「提交审核」 | toast「已提交审核,审核结果将在通知中告知」;状态「待审核」 | ☐ | |
| T3.6 🟡待实现·E2E-SUB-01 | admin /admin→投稿审核「发布到剧本库」 | 投稿状态翻「已发布」;/library 可搜到该剧本(跨用户可见) | ☐ | |
| T3.7 | 「需修改」流转 | admin 填意见→作者站内通知;投稿行显示「意见:X」,可「重新编辑提交」 | ☐ | |
| T3.8 | /studio 编剧档案创建 | 「编剧资料已创建」;/writers 可搜到该笔名 | ☐ | |
| T3.9 | /writers 搜索/题材 chip/排序/主页弹层 | 列表联动;主页「在库剧本」点击跳 /library | ☐ | |
| T3.10 | /showcase 发布作品(选已完成成片+命名) | 「作品已发布到作品墙」;卡片署名 by 昵称;自己的卡可「撤下」(confirm) | ☐ | |
| T3.11 🟢E2E-SPK-01 | /spark 「立即加入(赠 100 积分)」 | toast「已加入火花计划,100 积分已到账」;「✓ 已加入 · 当前阶梯「星火」」;重复加入 400「您已加入火花计划」 | ☐ | |
| T3.12 | 火花阶梯数据联动 | 投稿过审 N 部后:1-2 部星火 100/部、3-5 部炬火 150/部、6+ 燎原 200/部(可用 admin 改 submissions 状态驱动) | ☐ | |
| T3.13 | /viral-analysis 提交分析 | 「开始分析(5 积分)」→「分析完成」;总分 60-94;四维条+改写钩子可复制;流水 -5 | ☐ | |
| T3.14 | /evaluation 快速评估(100 字以上) | toast「已提交,冻结 100 积分」;三阶段流水线推进至完成;报告总分/七维/风险;流水先 -100 后结算 | ☐ | |
| T3.15 | 深度评估与失败退款 | 深度冻结 300;「字数不足」路径不扣费;失败路径「冻结积分已自动退回」流水 +N | ☐ |
T4 · 积分与充值闭环
| # | 动作 | 预期 | ☐ | 实际 |
|---|---|---|---|---|
| T4.1 | demo /recharge 输金额看预览 | 「按当前汇率 1 元 = 10 积分,预计获得 N 积分」实时联动 | ☐ | |
| T4.2 | 创建充值订单(如 100 元) | toast「充值订单 RCH… 已创建(1000 积分),请转账后上传支付凭证」;订单行状态「待审核」 | ☐ | |
| T4.3 | 「上传凭证」传图片 | toast「凭证已提交,等待审核」;凭证列出「查看」链接 | ☐ | |
| T4.4 | admin /admin→充值审核「通过」 | confirm 后 toast「已到账 1000 积分,返佣已结算」;demo 收到「💰 充值已到账」实时通知;余额 +1000 | ☐ | |
| T4.5 | 拒绝路径 | admin「拒绝」填原因→订单「已拒绝」+备注列显原因;不到账 | ☐ | |
| T4.6 | 金额入账恒等式对账 | demo /credits:充值行「+1000」,余额列=前行余额+1000 | ☐ | |
| T4.7 🟡待实现·E2E-RCH-01 | 充值→凭证→审核→到账全链自动化 | 金额入账恒等式断言(e2e 用例待写) | ☐ | |
| T4.8 | 满 980 元单自动开分销 | 单笔 ≥980 元审核通过后,demo 收「推广资格已自动开通」通知,/distribution 资格卡转绿 | ☐ | |
| T4.9 | /account 修改密码 | 「密码已修改」;其他已登录会话全部失效需重登;新密码可登 | ☐ | |
| T4.10 | /account 修改邮箱 | 成功后强制跳 /login;新邮箱可登、旧邮箱不可 | ☐ |
T5 · 企业与分销
| # | 动作 | 预期 | ☐ | 实际 |
|---|---|---|---|---|
| T5.1 | /enterprise-apply 提交申请(未登录) | 「申请已提交,等待审核…」 | ☐ | |
| T5.2 | admin /admin→企业审核「通过」 | toast「已通过」;申请人刷新后导航出现「企业管理」 | ☐ | |
| T5.3 | 邀请码加入 | owner 生成邀请码→新用户「使用邀请码加入」→「已加入企业」;成员列表出现 | ☐ | |
| T5.4 | 成员角色与权限组 | 改角色/建组/指派按 owner 专属收敛(成员不可见管理按钮);素材权限五项联动(勾删除自动含编辑) | ☐ | |
| T5.5 | 企业素材共享闭环 | 拆解台资产「共享到企业」→企业管理「企业素材库」可见;导演项目「共享到企业」→「共享项目」tab 可见 | ☐ | |
| T5.6 | 看板联动 | 组织充值/消耗后,看板四格与 14 天趋势、双榜更新 | ☐ | |
| T5.7 | 两级返佣到账结算 | 下线(一级)充值到账→上级流水「分销收益…佣金 8%」;下线的下线充值→上级 2% | ☐ | |
| T5.8 | 提现全状态机 | 申请(≥100)→admin 转审核中→批准打款→用户「确认到账」→已完成;中途「撤回」额度立即解锁 | ☐ | |
| T5.9 | 提现下限与驳回 | <100 toast「最低 100 积分起提」;admin 驳回填原因→用户行显原因 | ☐ | |
| T5.10 | 兑换 1:1 | 满 10 兑换成功「兑换成功」;生产积分到账;流水双行(收益-、积分+) | ☐ | |
| T5.11 🟡待实现·E2E-WD-01 | 提现申请→审核→划转自动化 | 双向台账恒等式断言(e2e 用例待写) | ☐ |
T6 · 管理后台(14 tab 人工走查)
🟢E2E-ADM-01 已覆盖概览/用户/任务审计/剧本四 tab 的只读巡检;本层走查写动作与其余 tab。
| # | 动作 | 预期 | ☐ | 实际 |
|---|---|---|---|---|
| T6.1 🟢E2E-ADM-01 | 概览/用户管理/任务审计/剧本管理四 tab 遍历 | 各 tab 渲染非空;零 5xx 零错误 toast | ☐ | |
| T6.2 | 用户「调整积分」 | prompt→confirm→「已调整」;用户积分与台账双更新;负数可扣 | ☐ | |
| T6.3 | 「重置密码」 | prompt ≥8 位(<8 拦截)→confirm→「已重置」;旧密码失效 | ☐ | |
| T6.4 | 「停用/启用」「设管理员」 | 状态 tag 翻转;停用后该用户无法登录;新管理员可见「管理后台」入口 | ☐ | |
| T6.5 | 积分发放+导出 | confirm「确认发放 N 积分?」→「发放成功」;CSV 下载可开 | ☐ | |
| T6.6 | 公告管理发布→前台横幅 | 「公告已发布」;前台顶栏下出横幅,「我知道了」后不再显示 | ☐ | |
| T6.7 | 模型配置「测试连接」 | 真实端点返回连通结果;「设为当前使用」后全站切换,删配置回退 Mock | ☐ | |
| T6.8 | 计费配置改图片单价 | 改为 12 保存→新生成流水按 12 计;示例行「5 秒视频 = …」联动 | ☐ | |
| T6.9 | 指令版本「设为生效」 | confirm「下一次拆解即使用该指令」;新拆解走新版 | ☐ | |
| T6.10 | 提示词管理编辑+上传 | 「已保存并生效,下一次任务即使用新提示词」;「还原为生效版内容」可用 | ☐ | |
| T6.11 | 任务审计「手动退款」「重试拆解」 | confirm 后 toast;退款流水出现;重试后任务重新推进 | ☐ | |
| T6.12 | 剧本管理批量上传 .txt | 「已上传 N 个剧本到「X」分类」;文件名即标题;/library 可见 | ☐ | |
| T6.13 | 剧本评分/封面/上下架/删除 | 评分 0-5 一位小数校验;下架后 /library 不可见 | ☐ |
T7 · 权限与边界
| # | 动作 | 预期 | ☐ | 实际 |
|---|---|---|---|---|
| T7.1 🟢E2E-AUTH-01 | 错密码登录 | 原生 alert「邮箱或密码错误」 | ☐ | |
| T7.2 🟢E2E-AUTH-01 | admin 登录成功跳转 | 进首页 /;登出回 /login | ☐ | |
| T7.3 🟢E2E-AUTH-01 | 未登录直访 /dashboard | 重定向 /login | ☐ | |
| T7.4 🟢E2E-AUTH-01 | demo 角色边界 | 导航无「管理后台」;直访 /admin 被顶回 / | ☐ | |
| T7.5 | demo 访问企业页边界 | 无 org 账号:导航无「企业管理/推广与收益」;直访 URL 显示未加入引导态 | ☐ | |
| T7.6 | 登录限流 | 连续错密码触发限流提示(5 次/时 register/20 次/分 login 量级) | ☐ | |
| T7.7 | 404 路由 | 访问不存在路径显示 404 兜底,不白屏 | ☐ | |
| T7.8 | 积分不足路径 | 低水位账号发起高扣费(如深度评估 300)→ toast「积分不足」,不产生半程数据 | ☐ | |
| T7.9 | 生成失败退款面 | (配置一个必失败端点或断网模拟)任务失败→流水自动「+N」退回行;页面状态「失败」 | ☐ | |
| T7.10 🟢E2E-NAV-01 | 全站 20 路由巡检(自动化) | 零未捕获 console error/零 5xx/零网络失败 | ☐ | |
| T7.11 | 视觉走查(纯人工) | 深浅两主题下全站主要页无破版;截图册 24 图与当前 UI 一致(重跑 npm run test:snapshots) | ☐ | |
| T7.12 | 通知与 SSE 面 | 拆解/视频/充值/工具完成均有实时 toast;铃铛未读数 60s 内更新;「全部已读」清零 | ☐ |
对账与回归锁说明
- 🟢 九场景:AUTH-01/DASH-01/GEN-01/GEN-02/CRD-01/ADM-01/NAV-01/SPK-01/SNAP-01——与本清单条目一一对应( grep
🟢对账);断言面详见 docs/test/e2e-qmtv-场景清单.md。 - 🟡 五场景(SUB-01/RCH-01/EXP-01/WD-01/DIR-01):写链自动化待升格——本清单 T3.6/T4.7/T1.13/T5.11/T2.17 的人工步骤即其未来断言主干。
- 计费数字以 使用说明书 附录 A 为准(源码核对口径);测试中发现的流水金额不符直接记 bug(标注条目号)。
- e2e 环境纪律(隔离/造数/禁 retry)见 docs/test/e2e-测试约定.md。
- FSD 分册对账(三十一期回填)——人工条目↔功能规格区间映射(规格原文 prd/fsd-qmtv/00-总则,13 册全量):T0→总则横切(T0.2→m01/T0.3→m12)|T1→m02 F2.01
F2.13(T1.13→F4.01 导出编排/T1.14→总则 §2.3)|T2→m03 F3.01F3.09(T2.72.9→m11 F11.01F11.02/T2.102.11 一键转剧→F10.15/T2.122.17→m10)|T3→m09 F9.01F9.08(T3.113.12→m13 F13.01/T3.133.15→m11 F11.06F11.08)|T4→m08 F8.01F8.03(T4.94.10→m01)|T5→m06 F6.01F6.14(T5.75.11→m07 F7.01F7.06)|T6→m12 F12.01F12.13|T7→m01 F1.01~F1.09(T7.5 边界→m06/T7.12→m05 F5.01)。纯前端/视觉条(T0.6/T0.8/T7.11 等)无 F-ID 可挂。
2.3 - QMTV 使用手册 · 文档索引
本目录面向最终用户与 QA;与开发面文档(DEPLOY.md/测试规划)互补。发布视图见 my-docs Hugo 站「QMTV 文档」分区。
| 文件 | 读者 | 用途 |
|---|---|---|
| QMTV使用说明书_中文.md | 工作台用户/企业管理员/平台管理员 | 页面图解版操作手册:24 路由五段卡片(入口→截图→做什么→操作步骤→规则与注意)+ 计费速查表 + FAQ |
| 人工测试清单.md | QA | 走查底稿:T0~T7 旅程分层 ☐/实际 填空表;🟢 已被 e2e 覆盖 / 🟡 待实现 / 无标纯人工 三态映射 |
| images/ | — | 截图 24 张(login/register/home/…/admin),由截图管线产出 |
截图重跑
截图纪律:纯只读遍历+截图(dashboard 仅填表单文本不提交,零造数);信号驱动 settle(innerText>80)。
构建发布
改本目录任何 .md 后:python3 docs/sync_docs.py --build——同步进 Hugo 站(qmtv/manual/ 分区)并过静态链接死链对拍;手册图片随 ASSET_RULES 一并复制。
关联文档
3.1 - QMTV 测试规划(Go 版,P0 底册)
回答移植需求②"基于现有项目先写单元测试——移植现有测试(如果有),或另写"。现状实测:冻结版零单测,仅 4 个 API 冒烟脚本(verify_*.js)覆盖 60/239 端点。结论:以 TDD 另写为主(pms 范式),verify 断言移植为集成冒烟底册,e2e 骨架按 pms 约定立。
1. 分层测试范式(对齐 pms backend)
| 层 | 手段 | 纪律 |
|---|---|---|
| repo 层 | sqlmock(DATA-DOG/go-sqlmock);newMockDB 4 行 helper 就地定义,无共享 testutil | 断言 SQL 形状 + ExpectationsWereMet;结尾必校验 |
| service 层 | 手写 fake repo(map 内存副本);关键纪律=fake 忠实 gorp 语义(Insert 后 version=原值+1 等回写行为必须模拟,防掩盖差一缺陷) | 纯函数尽量抽出独立测(计费/解析器/提示词组装) |
| handler 层 | 不测(薄层,零业务逻辑——域包四文件铁律保证) | 装配冒烟在 main_test.go |
| 守卫 | cmd/server 三件(not_null/pointer_field/status_table),schema 六原则机器守卫 | 负样本演练留档 TECH.md §9 |
| 集成冒烟 | //go:build integration 标签真库用例(对齐 pms test/smoke_test.go;MySQL 不可用自动 skip)+ verify 移植断言 | dev-up 环境跑 |
| 全局 | go test ./... -race -count=1 纯 mock 不连外部服务 | CI 缺省档卡点 |
2. 分域单测规划(先红后绿,TDD 顺序=P 期次)
| 域 | 期 | 必测纯函数/场景(首批) |
|---|---|---|
| auth | P1 | scrypt 往返(已有)、JWT 双格式(已有)、refresh 轮换状态机(签发→轮换→旧 token 拒→过期拒→改密全撤销)、注册送积分+邀请返佣、限流窗口边界 —— P1 已落地(service 11 场景 + repo sqlmock 7 + jwtsecret 3 + main_test 装配 4 + integration 全链/C8 互操作;限流 4 场景见 gateway 包) |
| credits | P1 | CAS 扣款成功/余额不足 402/版本冲突重试/退款 refunded 防重/balance_after 台账一致性/COSTS×settings 双层计费价 —— P1 已落地(billing 6 + repo sqlmock 6 + service fake 6 + integration 并发不超扣/台账对账/零台账);差异收口(PRD v1.3):CAS 谓词仅 credits>=? 下无版本冲突路径,RunWithRetry 定稿不引入;退款防重=credits.Refund 唯一口(F9),P2 W7 落地 |
| task | P2 | Mock 规则拆解(场次切分/人名提取/景别轮转——preprocess.js 逐规则移植对拍)、parser.parseSplit 宽容解析(60 镜上限/img 1200 字)、LLM 拆解失败回退链 —— P2 执行中(2026-10-18 立项) |
| generation | P2 | ratio→尺寸映射、videoCost(secs)、providers 三响应形态分发、轮询状态词判定、Mock SVG/WAV 字节头 —— P2 执行中 |
| export | P2 | 导出积分×段数、export_tasks 全链(期次随 TECH 域包表 P2,v1.3 对齐) |
| director/fusion/workbench | P3 | 双表通路 _tbl 路由(Go videoSmart 显式表参数,P2 期初定型)、队列 recoverQueue、审片双入口 |
| billing-admin | P4 | 充值到账+两级返佣、提现 7 态状态机、白名单键过滤(credit-settings PUT) |
3. verify_*.js 断言移植矩阵(约 60 端点 → Go 集成冒烟)
现有脚本逐断言移植为 Go 集成测试(integration 标签,dev-up 环境执行;脚本本身保留在冻结区不动)。「可测期次」= 该端点 Go 实现落地期。端点数为约数(唯一路径口径):分项 ~28+~20+~6+~9 含同端点多场景重复计数,去重后约 60。
| 源脚本 | 断言场景 | 端点数 | 可测期次 | Go 冒烟形态 |
|---|---|---|---|---|
| verify_v14.js | 火花 join/me/阶梯;工作台三工具+列表;经典审片+回读;导演项目→分镜→导演审片+资产队列挂起;组织申请→审批→邀请→加入→非 owner 改角色拒→项目共享→成员可见 | ~28 | P3(组织面 P4) | Go httptest 或真库 curl 链 |
| verify_v17.js | 企业看板/trend/双排行;writers 排序+主页;showcase 发布/撤下;全站搜索;剧本库新字段;素材夹+10GB 容量+assign;非 owner 建组拒;错密码改邮箱拒 | ~20 | P4 | 同上 |
| verify_eval.js | evaluations/config;txt 上传;GBK 非法字节回退不崩;深度评估 stage 推进轮询;报告下载含逐集证据 | ~6 | P3 | 同上(stage 推进用 pollUntil) |
| verify_lib.js | genres 配置;种子幂等回填;五维筛选;详情目录;rating/new 排序;管理端 CRUD+用户端 404 | ~9 | P3 | 同上 |
| 新增(脚本空白,各期验收补) | 登录/refresh 真轮换/改密;扣款 402/退款防重;拆解 v2 双引擎+回退;视频 v2 按秒计费;SSE 五类事件(Go 侧 client 断言帧格式);4 个 ZIP 端点结构;充值→审核→返佣→提现全链;模型接入 18 端点 | ~179 | 随 P1~P4 | 每期验收新增集成用例 |
4. E2E 约定骨架(正式版 P5,按 pms docs/test 三文档体系)
- 初始世界:独立库
qmtv_e2e(dev-up 变体建库导 full+seed);账号矩阵=seed 三账号+按需 API 注册;业务单据不进种子,一律 API 造数(防绕过状态机)。 - 外部 API 零真实调用(用户指令 2026-10-17,instructions.txt):e2e/集成测试局限在本系统内,外部 API 调用会引起费用——全部 mock。LLM/图像/视频供应商一律走 Mock 通道(测试环境不配 model_endpoints 的 api_key,或显式 provider_routing=mock);网络出口断言(providers 适配层的真实 HTTP 调用)用 httptest 本地假服务器验证协议形状,绝不打真实供应商;CI 环境保持零 Key(
llm.isAvailable()==false全站 Mock 路径),真实供应商连通性只允许人工在管理后台用「测试连接」单次验证。 - 确定性:时间用相对量;金额固定值;禁按自增 id 断言(qmtv 全字符串 ID 天然满足)。
- 隔离:
e2e-reset(DROP/CREATE+导入)/e2e-seed(快恢复);失败不自愈必须 reset;连跑两轮间隔 reset。 - 明令禁止:禁 sleep 等事件链(一律 pollUntil,超时 20s——评估 stage 推进/视频轮询场景);禁 retry;5xx 直接抛响应体。
- 场景登记:
docs/test/e2e-qmtv-场景清单.md行格式对齐 pms(场景ID E2E-XXX-NN|FSD 功能 ID|主干路径|关键断言|现状);缺陷修复覆盖优先并入现有场景加断言。 - Playwright 引入与否:P5 决策——前端冻结的前提下,API-first 断言册已覆盖回归需求;截图册(操作手册配图)可单独用 Playwright 走真 UI,不与断言册共用一行代码。(二十六期已落地:前端演进后 Playwright 双配置入位——断言册 11 用例〔auth 4/dashboard/generation 2/credits/admin/nav/spark;含 setup 册 14〕 + 截图管线,约定见 docs/test/e2e-测试约定.md;本条 P5「前端冻结」前提已随 TS 演进裁决失效,双配置即终态。二十七期勘误:二十五/二十六期宣称「13 用例」系计数漂移,11 为亲数权威口径。)
5. CI 闸门(scripts/ci.sh,已落地)
| 档 | 内容 | 时长 |
|---|---|---|
| –fast | build+vet+check_consistency+冻结契约守卫(frozen_contract_check)+守卫三件 | <1min |
| 缺省 | +全量 go test -race -count=1 | ~2min |
| –with-env | +dev-up+healthz+登录真断言+refresh 轮换断言(新旧 RT 不同/旧复刷 401)+query token 兜底 | +1min |
| –e2e | =–with-env 全部 + e2e 环境(qmtv_e2e/:3301/frontend/dist 托管)+ Playwright 断言册——显式 opt-in,浏览器二进制不进日常闸门(二十六期第四档) | +2min |
JUnit 双断言(场景总数≠期望也阻断)随 P5 e2e 落地引入。
3.2 - QMTV E2E 测试约定
二十六期(2026-10-19)立册。对齐 pms e2e 约定骨架,按 QMTV 单库场景裁剪。 场景覆盖地图见 e2e-qmtv-场景清单.md。
§1 初始世界
- 环境:
bash scripts/dev/e2e/e2e-up.sh一键起——独立库qmtv_e2e(DROP/CREATE 即全量 reset)+ 端口:3301(与 dev :3300 三隔离:库/端口/产物文件名,可共存)+ 托管frontend/dist(每次 up 强制重建,永测当前 src)。停环境e2e-down.sh;快恢复(不重建库)e2e-seed.sh。 - 种子自检:
e2e-verify.sh六项(healthz / admin·demo·producer 三账号登录 / models-active / style-presets)——绿才能跑用例。 - 账号矩阵:admin@qmtv.com(管理员,credits 100000)/ demo@qmtv.com / producer@xingmang.cn——密码=seed 公开演示值,不入新密。API 造数的辅助账号一律
e2e-前缀 email;storageState 册(.auth/{admin,demo}.json)由 setup 项目真实 UI 登录一次产出,用例零登录(对抗 login 20/min 限流)。 - 确定性规则:未配置
model_endpoints→ 生成类全走内置 Mock 同步 done(无轮询等待、无外部超时);Mock 图像/视频字节含随机源——只断结构确定(卡片存在/URL 可达/台账行),绝不断素材内容。 - 预置种子:SQL seed(admin/demo+分销配置+计费 KV)+ Go seed(producer+示例任务链)。业务单据不进种子,一律 API 造数。
- 隔离与清理:用例各自造数各自收尾(集成侧 integCleanup 同纪律);失败不自愈——重跑前
e2e-up;连跑两轮必须间隔 reset(credits 水位会漂移)。
明令禁止
- 禁 sleep 等待 UI 条件——唯一合法等待面是 Playwright 自动等待/
expect.poll/waitForEvent/pollUntil。豁免两处(登记):①巡检类用例的路由间静置窗口(nav/admin 的waitForTimeout(400/1500)、snapshots 截图前waitForTimeout(800)——错误收集窗/渲染落定窗,非断言等待);②截图前networkidle。 - 禁 retry——config
retries: 0(pms flaky 规则:禁 retry 掩盖;修复优先序 改等待>改定位>改断言>最后才是 retry)。 - 禁断全局计数/绝对余额——流水断言用 total 相对面 + 最新行身份;余额用
assertBal恒等式(后端集成侧)。 - 禁触 qmtv_main / pms_ / sw_main*——e2e 只操作 qmtv_e2e 单库(远程 139 共享实例纪律平移)。
- SSE 面禁 networkidle——EventSource 长连接使 networkidle 永不触发;巡检用 load+双 rAF+静置窗口。
§2 可测性硬规则
- 断业务结果,不断文案细节:toast 只断关键短语(「拆解完成」「去字幕完成」);选择器优先 role/placeholder/既有稳定 class,不批量加 testid(触碰面爆炸);高频锚点按需单点补并在此登记。
- 失败留证:trace
retain-on-failure+ screenshotonly-on-failure→e2e/.artifacts/(gitignore)。 - 断言/截图分文件一行不共用(pms 硬规则):断言册
playwright.e2e.config.ts(testDir e2e,smoke 项目收assertion/*.spec.ts);截图册playwright.config.ts(只收snapshots.spec.ts)。 - API 侧错误信封直接抛(
e2e/lib/client.tsapiReq):非 2xx 抛响应体——静默吞错=测试说谎。 - 对话框断言用
waitForEvent('dialog')(once+变体捕获有竞态——auth.spec 负样本抓获);LoginView 错误走原生 alert,业务 confirm 各页分散(WorkbenchView 仅 repaint 空描述分支)——用例写前先读该页 submit 源码,勿凭笔记推 dialogs。
§3 运行手册
§4 flaky 处理规则
- 72h 内同一用例红 ≥2 次 → 摘出开单,不得带红合入;
- 7 天未修 → 删用例(场景清单「自动化?」列同步回落「待实现」);
- 关键用例落地必配负样本演练(涉及金额/回归锁/A1 教训面——改断言键名/路径必红才转正;A1 fixture 死键教训:键名错→锚死力失效)。巡检类(nav/admin 只读遍历)可豁免——harness 判死力由其它用例的有机红佐证(二十六期如实报告:admin.spec 无单独负样本,nav/auth/credits/dashboard/generation 五处有机红已证)。
§5 场景登记与回归锁
- 场景 ID 唯一(
E2E-<模块>-<序号>),ID 标注于 spec 文件头注释(用例名可不带前缀)——清单与 spec 双侧 grep 对账按场景 ID 级(「自动化?=是」的行必须有对应 ID 的 spec 文件,清单「现状」列登记 ✅ 文件名)。〔二十七期勘误:原「用例同名对账」与实际不符——ID 在头注释非用例名,对账实为场景 ID 级 9=9+文件级 ✅ 列。〕 - 缺陷修复优先「并入现有场景加断言」(回归锁),不新开用例——本仓
/review-planA 类「验收锚点」判死力机制直接挂此。 - 写链场景(导出 ZIP、充值审核、分销提现等)先登清单「待实现」行,后续期次升格。
3.3 - QMTV E2E 场景清单(覆盖地图)
行格式:场景 ID 唯一;「自动化?=是」行与
frontend/e2e/assertion/*.spec.ts按场景 ID 级对账(清单 ID ↔ spec 文件头注释 ID+「现状」列 ✅ 文件名,与测试约定 §5 同口径;二十七期勘误:原「用例同名」与实情不符——用例 ID 在文件头注释非用例名)。 「FSD 功能 ID」列=场景↔功能规格双向对账锚(三十一期回填)——指向prd/fsd-qmtv/各册F<m>.<nn>节;横切场景(积分/导航/截图)无单册归属标「横切」。 二十六期首批冒烟 8 场景全走 Mock 同步面(避开 SSE 依赖与长轮询);写链场景登「待实现」后续升格。
| 场景ID | 场景名 | 模块/页面 | 前置种子 | 主干路径(系统视角) | 关键断言 | 冒烟? | 自动化? | FSD 功能 ID | 现状 |
|---|---|---|---|---|---|---|---|---|---|
| E2E-AUTH-01 | 登录/守卫/登出 | auth/LoginView+App 导航+router guard | 三账号 seed | 错密码 alert→admin 登录跳首页→未登录访 /dashboard 重定向→demo 无管理后台入口且直访 /admin 顶回→admin 可达 /admin 并登出回 /login | alert 文案「邮箱或密码错误」;URL 断言×4;权限分层入口可见性 | 是 | 是 | F1.01~F1.09(登录 F1.02) | ✅ auth.spec.ts |
| E2E-DASH-01 | 工作台拆解提交 | dashboard/DashboardView | admin 零任务 | POST /tasks/v2(Mock 同步)→toast→列表 invalidate | toast「拆解完成」;任务卡镜数>0(A1 回归锁:wire=驼峰 shotCount,误读蛇形恒 0 必红) | 是 | 是 | F2.03(V2 拆解)+F2.01(列表) | ✅ dashboard.spec.ts |
| E2E-GEN-01 | 独立图片生成 | independent-tools 图片 tab | admin 登录 | POST /images/generate(Mock 同步 done) | .img-card 即现且含提交提示词 | 是 | 是 | F3.08(图片素材) | ✅ generation.spec.ts |
| E2E-GEN-02 | 工作台工具提交 | workbench/WorkbenchView | API 造 standalone video(standalone:true 必传) | 选工具→选成片→提交→Mock 同步 done | toast「去字幕完成」(去字幕无 confirm——confirm 仅 repaint 空描述分支) | 是 | 是 | F11.01+F11.02(提交+轮询) | ✅ generation.spec.ts |
| E2E-CRD-01 | 积分流水闭环 | credits/CreditsView | admin 登录 | API 造图片生成→台账入账 | total 相对 +1;最新行含「独立图片生成(-10)」(不断首页行数——分页满页恒等碎) | 是 | 是 | 横切(总则 §2.3;台账 F1.06) | ✅ credits.spec.ts |
| E2E-ADM-01 | 管理端只读巡检 | admin/AdminView | seed 三账号 | 概览/用户管理/任务审计/剧本管理四 tab 遍历 | .stat≥5;用户行≥3;tab 面板渲染;零 5xx 零错误 toast | 是 | 是 | F12.01+F12.02(跨 m01/m02/m09 tab 注记) | ✅ admin.spec.ts |
| E2E-NAV-01 | 全站只读巡检 | 20 路由全页遍历 | admin storageState | 逐页 goto+load+双 rAF+静置窗 | 零未捕获 console error / 零 /api 5xx / 零 /api 网络失败(已抓获 spark 缺口) | 是 | 是 | 横切(全站路由) | ✅ nav.spec.ts |
| E2E-SPK-01 | 火花计划加入链 | spark/SparkView | admin 未加入(e2e-up 后初态) | UI join→me invalidate | 「已加入」标签现;API me.joined=true;重跑容错(残留轮直断投影面) | 是 | 是 | F13.01 | ✅ spark.spec.ts |
| E2E-SNAP-01 | 截图管线自检 | snapshots.spec.ts | admin storageState | 全站 24 图(公开 3+登录态 21;dashboard 填样例文本不提交;/export/:taskId 参数页豁免截列表态)落 docs/manual/images/ | PNG 字节非空+零重复字节(26 期四图→27 期 24 图扩容;settle 信号驱动 waitForFunction,公开页固定短 settle) | 是 | 是 | 横切(截图管线) | ✅ snapshots.spec.ts |
| E2E-SUB-01 | 投稿→过审→剧本超市上架 | studio+admin 投稿审核 | demo 投稿+admin 审 | POST submissions→admin approve→library 可见 | 状态机翻转+跨用户可见 | 是 | 待实现 | F9.03+F9.07(投稿+审核上架) | 二十七期候选 |
| E2E-RCH-01 | 充值凭证→审核入账 | recharge 双端 | demo 充值申请+admin 审 | POST recharge→upload 凭证→approve→余额+台账 | 金额入账恒等式 | 是 | 待实现 | F8.01~F8.03 | 二十七期候选 |
| E2E-EXP-01 | 成片导出 ZIP 下载 | export/ExportView | 已有 done 分镜 | GET-or-POST 导出编排→2s 轮询→ZIP 下载 | ZIP 魔数+分镜数 | 是 | 待实现 | F4.01(导出编排) | 后续候选 |
| E2E-WD-01 | 分销提现申请与审核 | distribution | 分销关系+佣金余额 | 提现申请→admin 审→余额划转 | 双向台账恒等式 | 是 | 待实现 | F7.03+F7.06(申请+审核) | 后续候选 |
| E2E-DIR-01 | 导演工作台分集拆解 | director3d | admin 登录 | 建项目→拆分集→分镜图生成 | director_shots 回填 | 是 | 待实现 | F10.01~F10.06(项目/分集/分镜图) | 后续候选 |
4 - 功能规格(FSD)
从 00-总则 进入(横切约定与分册地图);99-附录 是与六册的漂移差异台账;m01~m13 按模块分册(认证/任务/生成/导出/通知/组织/分销/充值/剧本库/导演/评估/管理后台/火花)。
4.1 - FSD M1 · 认证与用户
← 总则 | 附录 → | 上游:PRD §4 M1 实现落点:
backend/internal/domain/auth/(service.go/model.go/repo.go)+cmd/server/auth_handlers.go(handlers)+cmd/server/routes.go:34-51(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:73-230仅差异对拍参照。
M1 认证与用户
9 端点全量(基础信封见总则 §2.2:成功 {"ok":true,"data":…} / 失败 <码> {"error":"文案"})。本域零扣费、零 SSE、零素材上传;台账/通知为写入副作用(register/改邮箱两处)。
F1.01 注册
对应 PRD:§4 M1 · POST /api/auth/register(限流 5/h)| 状态:实现 | 实现落点:auth/service.go:68-140、auth_handlers.go:173-188 | UI:
/register(RegisterView)| 场景挂钩:无(e2e 未覆盖注册链,人工清单 T0 具名账号由 seed 产出)
1 功能定义
创建用户账号并签发首个会话(access + refresh 双令牌):初始积分发放、邀请码建分销关系、欢迎通知三件副作用一次完成。
2 触发条件与前置状态
- 未登录访问
/register提交表单 →POST /api/auth/register(公开面,WindowRateLimit("register", 5, time.Hour)——同 IP 每小时 5 次,超出 429)。 - 前置:邮箱未被注册;降级模式(无 DSN)下 503「数据库未配置」。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| JSON body | 是 | 非空(邮箱和密码必填)→ 严格正则 ^[^@\s]+@[^@\s]+\.[^@\s]+$(F7 统一版;冻结 register 原损坏正则被取代,PRD 差异已登记)→ 唯一性(邮箱已被注册)。三关任一失败 400 | |
| password | JSON body | 是 | 非空 + len ≥ 8(密码至少 8 位)400 |
| nickname | JSON body | 否 | 缺省取邮箱 @ 前缀 |
| invite_code | JSON body | 否 | 命中 referral_code 建分销关系;不命中静默跳过(不报错,冻结同) |
4 处理流程
flowchart TD
A["POST /auth/register"] --> B["四关校验"]
B --> C["invite_code 查询邀人<br/>(不命中静默)"]
C --> D["建 users 行<br/>role=user / status=active / workspace_version=classic<br/>referral_code=QM+6位base36大写(crypto/rand)"]
D --> E{"有邀人?"}
E -->|是| F["INSERT referral_relations"]
E -->|否| G["跳过"]
F --> G
G --> H["台账 amount=0 / balance_after=初始积分<br/>type=register 描述「注册赠送 200 积分(初始)」"]
H --> I["欢迎通知(积分数字跟随 trial 实际值)"]
I --> J{"有邀人?"}
J -->|是| K["邀人台账 +50 type=referral"]
J -->|否| L["跳过"]
K --> L
L --> M["issueSession → 200 {ok,data:{token,refreshToken,user}}"]5 输出与结果状态
200 {"ok":true,"data":{"token","refreshToken","user":PublicUser}}(AuthSession 统一会话体,model.go:78-82)。- user 投影字段集(冻结 publicUser 逐字节,model.go:33-44):
id/email/nickname/role/credits/org_id/workspace_version/referral_code/affiliate_enabled/created_at;org_id空串与referral_codeNULL 均→JSON null。
6 状态流转
无状态机。用户初始 status='active';workspace_version='classic'(后续 F1.09 可改)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| email/password 缺失 | 400 | 邮箱和密码必填 |
| 邮箱不过正则 | 400 | 邮箱格式不正确 |
| 密码 <8 位 | 400 | 密码至少 8 位 |
| 邮箱已注册 | 400 | 邮箱已被注册 |
| 限流超 5 次/时 | 429 | 限流信封(gateway.WindowRateLimit) |
| trial_credits 配置为 N(默认 200) | 实发 N 积分;欢迎通知文案跟随 N;台账描述硬编码「注册赠送 200 积分(初始)」不跟随(冻结 index.js:92 同 quirk 保真)→ DIF-28-1 | — |
| body 非 JSON | 400 | 请求体格式错误(handlers 层,非哨兵) |
8 权限与数据规则
- 权限:公开面(限流替代鉴权);写入
users/referral_relations/credit_records/notifications四表。 - 数据规则:
users.emailUNIQUE;referral_codeUNIQUE(碰撞时 INSERT 报错与冻结一致,crypto/rand 36^6 空间);invited_by哨兵空串(冻结写 null,DDL NOT NULL 差异已登记);credits初始 = trial_credits settings 值(F6 Billing.TrialCredits——冻结硬编码 200 属刻意差异)。
F1.02 登录
对应 PRD:§4 M1 · POST /api/auth/login(限流 20/min)| 状态:实现 | 实现落点:auth/service.go:143-156、auth_handlers.go:195-208 | UI:
/login(LoginView——错密码 alert「邮箱或密码错误」,登录成功跳/)| 场景挂钩:E2E-AUTH-01(断言册 auth.spec.ts)
1 功能定义
邮箱+密码换取会话双令牌;不存在与密码错合并同一 401(不暴露账号存在性),停用账号 403。
2 触发条件与前置状态
- 未登录(或会话过期)提交登录表单 →
POST /api/auth/login(公开面,限流 20/min)。 - 前置:用户存在且
status='active'。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| JSON body | 是 | 不存在与密码错合并 401(冻结 index.js:100-106 语义) | |
| password | JSON body | 是 | bcrypt 校验失败 401 |
4 处理流程
flowchart TD
A["POST /auth/login"] --> B["ByEmail 查用户"]
B -->|不存在| C["401 邮箱或密码错误"]
B -->|存在| D["VerifyPassword"]
D -->|不符| C
D -->|符合| E{"status=active?"}
E -->|否| F["403 账号已被停用"]
E -->|是| G["issueSession<br/>access JWT(claims 携 rol)+ rt_ 刷新令牌<br/>原文返回前端 / SHA-256 落库 refresh_tokens"]
G --> H["200 {ok,data:AuthSession}"]5 输出与结果状态
同 F1.01(AuthSession)。JWT claims 含 rol(AdminMiddleware 零查库鉴权——角色变更需重登生效,PRD §3 登记);access TTL 72h。
6 状态流转
无状态机;refresh_tokens 表新增一行(revoked_at NULL / replaced_by 空)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 邮箱不存在 | 401 | 邮箱或密码错误(合并语义) |
| 密码错误 | 401 | 同上 |
| 账号停用(status≠active) | 403 | 账号已被停用(登录时才知道身份,区别于 F1.05 的 401) |
| 限流超 20 次/分 | 429 | 限流信封(e2e 扩量场景需留意,遗留⑤登记) |
8 权限与数据规则
- 权限:公开面;读
users、写refresh_tokens。 - 数据规则:refresh token 原文不落库(SHA-256
token_hash+expires_atLocalTime);每次登录新增一行(不清理旧行,过期行由ValidByHash谓词自然失效)。
F1.03 令牌刷新(F1 真轮换)
对应 PRD:§4 M1 · POST /api/auth/refresh | 状态:实现 | 实现落点:auth/service.go:159-179、auth_handlers.go:214-227 | UI:前端 api 层 401 自动触发(frontend/src/api.ts:36-44)| 场景挂钩:ci –with-env refresh 轮换断言(新旧 RT 不同/旧复刷 401)
1 功能定义
用 refreshToken 换新会话:旧令牌撤销、新令牌串链(replaced_by)——同一 refreshToken 只能用一次;与冻结版差异(冻结 refresh 恒 401 无新字段),属 PRD F1 刻意差异。
2 触发条件与前置状态
- 前端任何 API 401 → 自动 POST
/api/auth/refresh重试一次;或手动调用于POST /api/auth/refresh(公开面无限流)。 - 前置:refreshToken 未撤销、未过期、属主
status='active'。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| refreshToken | JSON body | 是 | SHA-256 后查 ValidByHash:不存在/已撤销/过期任一 → 401 invalid refresh token;属主停用 → 同 401(安全收敛) |
4 处理流程
flowchart TD
A["POST /auth/refresh"] --> B["SHA-256 查 ValidByHash"]
B -->|无效| C["401 invalid refresh token"]
B -->|有效| D["查属主 + 停用校验"]
D -->|停用/不存在| C
D -->|active| E["issueSession 签新对"]
E --> F["撤旧行:revoked_at 置位 + replaced_by=新 RT"]
F --> G["200 {ok,data:AuthSession}"]5 输出与结果状态
200 {"ok":true,"data":AuthSession}——refreshToken 字段含新令牌(冻结版响应只有 {token,user},无此字段——前端 W9a 同期消费)。旧令牌自此 401。
6 状态流转
refresh_tokens 行级状态机:active(revoked_at NULL)→ rotated(revoked_at 置位+replaced_by 串链);无单向状态图面,串链即审计线索。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| body 空/字段空 | 401 | invalid refresh token |
| 已用过的旧令牌二刷 | 401 | 同上(轮换唯一性) |
| 过期令牌 | 401 | 同上 |
| 属主已停用 | 401 | 同上(非 403——不暴露身份) |
8 权限与数据规则
- 权限:公开面;读
refresh_tokens/users、写refresh_tokens(撤旧)。 - 数据规则:原文仅存在响应体,库中只存 SHA-256(泄库不可逆推);
replaced_by构成轮换链。
F1.04 登出
对应 PRD:§4 M1 · POST /api/auth/logout | 状态:实现 | 实现落点:auth/service.go:258-260、auth_handlers.go:240-248 | UI:顶栏退出按钮(LoginView 登出回
/login)| 场景挂钩:E2E-AUTH-01(admin 可达 /admin 并登出回 /login)
1 功能定义
撤销当前用户全部 refreshToken(F1 增强;冻结版无服务端动作,PRD v1.2 补登差异)——改密/登出后所有已发刷新令牌一并失效。
2 触发条件与前置状态
- 已登录点击退出 →
POST /api/auth/logout(Bearer)。 - 前置:Bearer 有效;降级模式 503。
3 输入与校验
无 body。身份取自 JWT claims user_id。
4 处理流程
POST /auth/logout → RevokeAllForUser(userID)(UPDATE refresh_tokens SET revoked_at=NOW() WHERE user_id=? AND revoked_at IS NULL)→ 200 {"ok":true,"data":true}。
5 输出与结果状态
200 {"ok":true,"data":true};该用户全部未撤销 refresh_tokens 行 revoked_at 置位。
6 状态流转
同 F1.03 行级状态:active → rotated(批量)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无刷新令牌在册 | 仍 200(UPDATE 零行非错误) | — |
| 停用用户带有效 Bearer 调用 | 401 unauthorized(authRequired 层拦截,见 F1.05) | — |
8 权限与数据规则
- 权限:登录态(amw);只写
refresh_tokens自己的行(user_id 来自 claims,非入参——不可越权撤他人)。
F1.05 当前用户信息(me)
对应 PRD:§4 M1 · GET /api/auth/me | 状态:实现 | 实现落点:auth/service.go:182-191、auth_handlers.go:229-238 | UI:顶栏积分胶囊/角色菜单数据源| 场景挂钩:E2E-AUTH-01(间接:全站登录态面)
1 功能定义
返回当前登录用户公开投影(PublicUser);已登录态遇停用账号返回 401(P2 期首核查 A1 恢复冻结 util.js:53 全 authed 拦截语义,触发前端登出链路)。
2 触发条件与前置状态
- 前端挂载/刷新/积分变动后拉取 →
GET /api/auth/me(Bearer)。 - 另有 query token 兜底形态
GET /api/auth/me?token=(ci 冒烟 C3 裁决面——与 SSE/images 同族,仅此三处)。
3 输入与校验
无参数。身份取自 claims。中间件层已验签(JWTAuth.AuthMiddleware);service 层仅补停用校验。
4 处理流程
GET /auth/me → ByID(userID) → 停用? →401 : →u.Public() 投影 → 200 {"ok":true,"data":PublicUser}。
5 输出与结果状态
字段集同 F1.01 §5。credits 为当前实时余额(DECIMAL 读侧 float64)。
6 状态流转
无。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 用户已被停用(曾登录) | 401 unauthorized——区别于登录时 403「账号已被停用」;前端 401→refresh→仍停用→登出 | — |
| JWT 过期/伪造 | 401 unauthorized(中间件层,json 键亦 error) | — |
8 权限与数据规则
- 权限:登录态;只读自己一行(claims 绑定)。
F1.06 积分流水分页
对应 PRD:§4 M1 · GET /api/auth/credit-records | 状态:实现 | 实现落点:auth_handlers.go:309-324(委托 credits.Records)| UI:
/credits(CreditsView)/ 顶栏积分弹层| 场景挂钩:E2E-CRD-01
1 功能定义
当前用户全局积分台账分页读(每页 20,created_at DESC,id DESC)——所有域的扣费/退款/充值/佣金行共用此读面。
2 触发条件与前置状态
- 进入积分流水页/翻页 →
GET /api/auth/credit-records?page=N(Bearer)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| page | query | 否 | 缺省 1;非数字或 ≤0 折 1(不报 400,handlers.go:313-318 quirk) |
4 处理流程
GET /auth/credit-records → page 归一 → credits.Records(userID, page) → 200 {"ok":true,"data":{"list":[…],"total":N,"page":N}}。
5 输出与结果状态
list行字段:台账行投影(type/desc/amount/balance_after/created_at/task_id 等,credits 域读侧投影)。total全量行数(非当前页);page回显归一后值。
6 状态流转
无(纯读)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| page=abc / page=0 / page=-1 | 折 1 返回第一页 | — |
| 空台账 | list:[]、total:0 200 | — |
8 权限与数据规则
- 权限:登录态;WHERE user_id=claims 绑定(看不到他人流水)。
- 数据规则:排序
created_at DESC,id DESC(同秒稳定序);分页 size 固定 20。
F1.07 修改密码
对应 PRD:§4 M1 · POST /api/auth/change-password | 状态:实现 | 实现落点:auth/service.go:194-217、auth_handlers.go:255-267 | UI:
/account(AccountView 安全区)| 场景挂钩:无独立 e2e(人工清单 T7 掉线重登路径)
1 功能定义
验原密码后更新密码哈希,并撤销全部 refreshToken(F1:改密后所有已登录会话的刷新能力失效,access 令牌存续至 72h 自然过期)。
2 触发条件与前置状态
- 账号设置页提交改密表单 →
POST /api/auth/change-password(Bearer)。 - 前置:status=‘active’(停用 401,A1 拦截——本就查库零成本恢复冻结语义)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| old_password | JSON body | 是 | bcrypt 验证失败 400 原密码错误 |
| new_password | JSON body | 是 | 空/len < 8 400 新密码至少 8 位 |
校验序:查用户→停用拦→验原密码→验新密码(十六续 A1 修序先例在 M12 admin 域;本域顺序冻结逐字)。
4 处理流程
POST /auth/change-password → 停用?→401 : 验旧密码→验新密码→UpdatePassword(新 bcrypt 哈希)→RevokeAllForUser→200 {"ok":true,"data":true}。
5 输出与结果状态
200 {"ok":true,"data":true};users.password_hash 更新;该用户全部 refresh_tokens 撤销(前端引导重登)。
6 状态流转
无用户级状态机;refresh_tokens 批量 active→rotated。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 原密码错 | 400 | 原密码错误 |
| 新密码 <8 位或空 | 400 | 新密码至少 8 位 |
| 停用态调用 | 401 | unauthorized |
8 权限与数据规则
- 权限:登录态;只改自己(claims 绑定)。
F1.08 修改邮箱
对应 PRD:§4 M1 · PUT /api/auth/email | 状态:实现 | 实现落点:auth/service.go:220-244、auth_handlers.go:274-287 | UI:
/account(AccountView 安全区)| 场景挂钩:无独立 e2e(FAQ「改密登出」同族人工面)
1 功能定义
需当前密码确认的邮箱变更;成功后发站内通知(冻结 index.js:127-136 文案)。登出不联动(改邮箱不清会话,与改密不同——冻结语义保真)。
2 触发条件与前置状态
- 账号设置页提交改邮箱表单 →
PUT /api/auth/email(Bearer)。 - 前置:新邮箱未被任何账号占用。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| password | JSON body | 是 | 当前密码验证失败 400 当前密码错误 |
| new_email | JSON body | 是 | 非空+严格正则(F7 统一版)400 新邮箱格式不正确 → 唯一性 400 该邮箱已被占用 |
4 处理流程
PUT /auth/email → 停用拦 → 验当前密码 → 验新邮箱格式 → 唯一性 → UpdateEmail → 通知「登录邮箱已变更/登录邮箱已变更为 X,请使用新邮箱登录。」→ 200 {"ok":true,"data":{"email":"<新邮箱>"}}。
5 输出与结果状态
data.email = 新邮箱;users.email 更新;notifications 新增一行(标题+文案冻结逐字)。
6 状态流转
无。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 当前密码错 | 400 | 当前密码错误 |
| 新邮箱格式非法 | 400 | 新邮箱格式不正确 |
| 新邮箱已被占用 | 400 | 该邮箱已被占用 |
| 停用态调用 | 401 | unauthorized |
8 权限与数据规则
- 权限:登录态;只改自己。
- 数据规则:
users.emailUNIQUE 兜底;通知文案与新邮箱值拼接(无模板)。
F1.09 工作台版本状态
对应 PRD:§4 M1 · PUT /api/auth/workspace-state | 状态:实现 | 实现落点:auth/service.go:247-255、auth_handlers.go:289-306 | UI:工作台切换入口(双工作台
shot._tbl通路的选择器)| 场景挂钩:无(M2 拆解域间接消费)
1 功能定义
保存用户工作台版本偏好(users.workspace_version);非 'legacy' 值一律折 'classic'(白名单外静默归一,冻结 index.js:215 逐字)。
2 触发条件与前置状态
- 前端切换工作台版本 →
PUT /api/auth/workspace-state(Bearer)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| version | JSON body | 是 | body 字段名是 version(非 workspace_version——冻结 quirk);值 ≠legacy 一律落 classic,无 400 面 |
4 处理流程
PUT /auth/workspace-state → 归一(≠legacy→classic)→ UpdateWorkspaceVersion → 200 {"ok":true,"data":{"workspace_version":"<归一值>"}}。
5 输出与结果状态
data.workspace_version 为归一后值(传 bogus 得 "classic");users.workspace_version 持久化。
6 状态流转
classic ⇄ legacy 二值域,无第三态。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
传任意非法值(空串/bogus/null) | 静默落 classic,200 | — |
仅 legacy 字面 | 落 legacy | — |
8 权限与数据规则
- 权限:登录态;只改自己。
- 数据规则:M2 拆解域读此列决定 shots/director_shots 双表通路归属(
shot._tbl约定——见总则 §1 术语)。
M1 端点覆盖对账
| # | 端点 | 功能节 |
|---|---|---|
| 1 | POST /api/auth/register | F1.01 |
| 2 | POST /api/auth/login | F1.02 |
| 3 | POST /api/auth/refresh | F1.03 |
| 4 | POST /api/auth/logout | F1.04 |
| 5 | GET /api/auth/me | F1.05 |
| 6 | GET /api/auth/credit-records | F1.06 |
| 7 | POST /api/auth/change-password | F1.07 |
| 8 | PUT /api/auth/email | F1.08 |
| 9 | PUT /api/auth/workspace-state | F1.09 |
9/9 对账 PRD §4 M1(DIF 新发现:DIF-28-1 注册台账描述硬编码——见 99-附录 B)。
4.2 - FSD M10 · 导演与融合
← 总则 | 附录 → | 上游:PRD §4 M10 实现落点:
backend/internal/domain/director/(model/split/repo/service/service_assets/export 单包多文件)+cmd/server/director_handlers.go(28 handler)+ generation 侧双表通路三锚点(model.go:56-69 ShotView.Tbl / service_video.go:191-229 DirectorShotVideo / repo.go:261-283 LinkShotVideo)+cmd/server/routes.go:203-238(挂载)。事实基准=Go 代码;冻结版qmtv/server/director.js(11)/fusion.js(17)仅差异对拍参照。 域内要点:双表通路——本域写director_shots(M2 拆解台是shots表勿混),生成链经ShotView.Tbl显式表名打回;扣费面六处(三处硬编码照抄);ZIP 导入导出;wire 全蛇形+派生视图尾插键;对账口径:routes.go:209/:223 注释「11+17」系冻结源文件口径(#1/#2 冻结在 fusion.js:221/:227 却排导演注释块),物理分组=导演 13+融合 15;PRD §4 M10 已登记差异 ①~⑮(prd-qmtv-v1.md:208)+v1.12 变更行三件(二十续 A1 form-tag/A2 空数组/A3 episodes 404 文案)引用不重立。ID 前缀 dpr_/dep_/dir_(tasks 侧 task_id=‘dir_’+project_id 约定=PRD ⑩)。
M10 导演与融合
28 端点全量。四零域反转:本域扣费面六处为全站最密(extract 总额扣+单价退/generate-image/fusion 硬编码 3/convert 硬编码 30/remix/资产生成);split 双阈值 1200·800 合参(PRD ①);审片入口在 M11(:266-267)share 在 M6 组(:115,冻结 index.js:574)——同前缀勿误收。
F10.01 项目列表与创建(director3d GET/POST)
对应 PRD:§4 M10 · GET/POST /api/director3d | 状态:实现 | 实现落点:director_handlers.go:21-52、service.go:110-137、repo.go(projects 段)| UI:Director3DView 项目列表 | 场景挂钩:E2E-DIR-01(待实现,清单 :21——建项目→拆分集→分镜图生成)
1 功能定义
导演工作台项目面:列表(带分集预览)与创建(即时双阈值拆分集)。
2 触发条件与前置状态
登录态;create 前置 script_text 必填 400「剧本文本必填」(model.go:161,fusion.js:229——创建端点落在 fusion.js 冻结源,对账口径注记见册尾)。
3 输入与校验
{name, style_id, face_style, script_text, kind}(handlers :36-42;style_id 非空透传无白名单——vs face_style 白名单 service.go:130-132,quirk);name/style_id 截断照冻结 fusion.js:232。
4 处理流程
create:INSERT dpr_ 行 → SplitEpisodes(script, 800)(fusion 阈值;director resplit 用 1200——两处刻意不同源勿统一,split.go:98-111 合参)逐集 INSERT dep_(标题缺省「第N集」)。
5 输出与结果状态
列表=ProjectRow+episodes 尾插(service.go:110-113);create 返回 {id, episodes: n} 形态(照冻结 :227-236)。
6 状态流转
director_projects 无状态列;分集数创建即定(resplit 才重切)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| script_text 空 | 400 | 「剧本文本必填」 |
| 无「第X集」标记 | 按长度 800 块切分(chunkBlocks,split.go:103-105) | — |
8 权限与数据规则
authed + amw;列表按 user_id 属主过滤;org_shared 列随行下发(M6 共享面消费)。
F10.02 项目详情/删除/重切分(:id GET/DELETE、resplit POST)
对应 PRD:§4 M10 · GET/DELETE /api/director3d/:id、POST /api/director3d/:id/resplit | 状态:实现 | 实现落点:director_handlers.go:80-112、service.go:139-183、repo.go:158-184 | UI:Director3DView | 场景挂钩:E2E-DIR-01(拆分集段)
1 功能定义
项目详情(含分集树)、删除(级联两删序)、重切分(1200 阈值重跑)。
2 触发条件与前置状态
属主校验:非本人 403「forbidden」(model.go:146);不存在 404「not found」(:145)——文案分叉:『episode not found』系 extract/shots/convert 三处共享哨兵默认值(model.go:147),本面经 A3 重映射不直出。
3 输入与校验
resplit 前置项目存在+属主;script_text 不接受(重切用库内原 script——冻结同形)。
4 处理流程
- 详情:项目+分集树(episodes 尾插)+style_name JOIN(projectWithStyle service.go:212-215)。
- 删除:按 episode IN 子查询级联删 shots→episodes→项目(repo.go:158-166)——与 resplit 两删序不同(resplit 按 project_id 直删 :182-184),quirk 照抄冻结 :85-88 vs :95-96。
- resplit:删旧分集+分镜(按 project 直删)→
SplitEpisodes(script, 1200)重插(service.go:165-176;1200 阈值——PRD ①)。
5 输出与结果状态
详情=ProjectWithEpisodes;删除/重切恒 true(无 404 面——quirk:删除查在先恒 ok service.go:149-157)。
6 状态流转
分集/分镜随 resplit 整体重建(旧 shots 含 video_id 一并清除——重建语义照冻结)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非本人项目 | 403 | 「forbidden」 |
| 项目不存在 | 404 | 「not found」 |
| resplit 后为空文 | 单分集兜底(chunkBlocks 全文一块) | — |
8 权限与数据规则
authed + amw 属主面;级联删除顺序固定禁事务(中途失败半态=冻结同形)。
F10.03 分集详情(episodes/:eid GET)
对应 PRD:§4 M10 · GET /api/director3d/episodes/:eid | 状态:实现 | 实现落点:director_handlers.go:113-123、service.go:186-232、repo.go(episode JOIN 段)| UI:Director3DView L3 分集工作台 | 场景挂钩:E2E-DIR-01(工作台装载)
1 功能定义
分集工作台装载面:分集行+全部分镜(含图/视频产物 URL)+资产三组,一响应全量。
2 触发条件与前置状态
URL 无 :id 项目段——属主按 ep.project_id → projects.user_id JOIN 判:非本人 403「forbidden」(service.go:231;冻结 director.js:117 逐字);不存在 404「not found」(:222-226 本路由将共享哨兵 ErrEpisodeNotFound 重映射为 ErrNotFound——二十续 A3:冻结 :116 此面专属 ’not found’;’episode not found’ 仅 extract/shots/convert 三处直出)。
3 输入与校验
路径 :eid 无格式校验;无分页(全量分镜一次下发)。
4 处理流程
episodes 行 → shots 按 slot_no 序(每镜 slots 槽位组 service.go:267-278)→ assets 按 kind 三组 characters/scenes/props(:280-285)→ 图/视频经 imageFilePath/videoFilePath 解析 URL(export.go:137-152,store.Read 存在性守卫)。
5 输出与结果状态
{episode, shots:[{...shot, fields, slots, image_url?, video_url?}], assets:{characters, scenes, props}} 键形全蛇形。
6 状态流转
零写面;shot.status 消费面(processing/video_done 等由 F10.06/07 写入)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 分集不存在 | 404 | 「not found」(A3 重映射 ErrEpisodeNotFound→ErrNotFound;’episode not found’ 仅 extract/shots/convert 三处直出) |
| 跨项目访问他人分集 | 403 | 「forbidden」(属主按 project 判) |
8 权限与数据规则
authed + amw;assets 三组按 kind 聚合(本域 task_id=‘dir_’+project_id 约定隔离——PRD ⑩,M6 projectRank 依赖此形态)。
F10.04 资产提取(extract-assets POST)
对应 PRD:§4 M10 · POST /api/director3d/:id/episodes/:eid/extract-assets | 状态:实现 | 实现落点:director_handlers.go:124-140、service.go:453-490、repo.go(assets 段)| UI:Director3DView「提取资产」| 场景挂钩:E2E-DIR-01(集成实测 5 目标——含锚点前残余段 quirk)
1 功能定义
从分集文本提取角色/场景/道具三组资产并逐张生成立绘占位图:总额一次扣+逐张失败退单价 continue(PRD ⑨)。
2 触发条件与前置状态
属主双校验:项目+分集(eid↔project 无关联校验例外面——本端点校验关联,PRD ⑥ 注convert 例外);无可提取资产 400「没有可提取的资产」(model.go:148)。
3 输入与校验
无 body;目标数由 splitScenes 解析决定。
4 处理流程
DeleteAssetsByTask 先清后插(service.go:468——重提取抹掉含导入的全部项目资产,quirk 照抄冻结 :153)→ 总额 ImageCost×N 一次 TryDeduct「导演工作台资产提取 ×N(-M)」(:461-464,taskKey=‘dir_’+pid)→ 逐张 Generate 失败:域内 Refund 唯一口退单价(nil claim 无防重面)+continue(:474-480,文案「资产图提取失败自动退回(+单价):{err≤80}」)→ 风格前缀拼接 prompt(service.go:466,vs 资产生成后缀拼接——两源刻意不同,service_assets.go:109-117 注)。
5 输出与结果状态
{created: n, assets:{characters, scenes, props}}(同 F10.03 三组形态)。
6 状态流转
assets 行即插即生成(无队列)——与 F10.11 队列制不同源。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 零目标 | 400(扣费前校验) | 「没有可提取的资产」 |
| 单张生成失败 | 退单价+跳过,不整单失败(continue) | 台账「资产图提取失败自动退回(+10):…」 |
| 「道具:XX」行 | 「道具」命中 character 正则(charColonRe [\p{Han}]{2,3}[::] service.go:26 无排除——PRD ③) | — |
| 「第一集」锚点前残余段 | 亦成场景目标(lookaheadSplit 残余段入 raw,split.go:40-42——PRD ②) | — |
8 权限与数据规则
authed + amw 属主面;退款唯一口在域内 Refund(nil claim——extract 无认领制,与视频 ClaimVideoRefund 不同制)。
F10.05 分镜生成与编辑(shots POST、shots/:id PUT)
对应 PRD:§4 M10 · POST /api/director3d/:id/episodes/:eid/shots、PUT /api/director3d/shots/:id | 状态:实现 | 实现落点:director_handlers.go:141-166、service.go:235-405、repo.go:235-244 | UI:Director3DView L3 分镜卡 | 场景挂钩:E2E-DIR-01(分镜编辑段)
1 功能定义
分镜批量生成(从场景草稿落 director_shots)与单镜编辑(image_prompt/duration 等槽位)。
2 触发条件与前置状态
生成前置:eid↔project 无关联校验(service.go:309 注「逐字」PRD ⑥——仅校验各自存在+项目属主);编辑前置:分镜属主(经 JOIN director_projects)。
3 输入与校验
生成 body {scope:[]} 按场景 no 过滤(handlers :128-131;空=全部);编辑 body ShotInput 蛇形(service.go:392-400);duration clamp 1-15s(videoCost 同参)。
4 处理流程
生成:SplitScenes 草稿(有标记 title=首行 slice40;无标记 rune>10 空行块+「第N场:未命名场景」,split.go:113-137)→ InsertShot 15 列 15?(repo.go:235-244——冻结字面 0 错位修后语义,PRD ④:resolution=草稿值、attempts=0)→ 分镜槽位组插入。
编辑:COALESCE 部分更新+video_id 不动(video 产物编辑不清)。
5 输出与结果状态
生成 {created: n};编辑恒 true;响应行键形蛇形(ShotRow model.go)。
6 状态流转
status 初始 ‘draft’(InsertShot 修后语义)→ F10.06 图生成 ‘processing’→(图成)→ F10.07 视频链 ‘video_done’。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| eid 与 :id 项目不匹配 | 仍执行(无关联校验,PRD ⑥ quirk) | — |
| 编辑非本人分镜 | 404/403 | DirectorShotForUser 查无「not found」 |
| 空场景文本 | 零分镜插入,created=0 | — |
8 权限与数据规则
authed + amw 属主面;本域写 director_shots(M2 拆解台写 shots——双表通路归属标注,总则 §1 词条)。
F10.06 分镜图生成(generate-image POST)
对应 PRD:§4 M10 · POST /api/director3d/shots/:id/generate-image | 状态:实现 | 实现落点:director_handlers.go:167-181、service.go:407-425 | UI:Director3DView 分镜卡「生成图」| 场景挂钩:E2E-DIR-01(分镜图生成段)
1 功能定义
单镜立绘/Storyboard 图生成:ImageCost 动态价扣费→generation.Generate(Mock SVG 或真实供应商)→images(kind=storyboard)+image_histories 落链。
2 触发条件与前置状态
分镜属主(DirectorShotForUser JOIN——ShotView.Tbl=‘director_shots’ 显式表名,generation/repo.go:261-275);余额不足 402。
3 输入与校验
无 body(prompt 由分镜 image_prompt 组装+风格前缀);provider 侧 size 白名单/4000 截断(generation/provider.go 通用守卫)。
4 处理流程
TryDeduct ImageCost「导演分镜图生成(-{cost})」(service.go:410-413)→ gen.Generate → 成功:images 行+InsertImageHistory(ih_ 前缀——ids.PrefixImageHistory director/repo.go:319 消费,与 task 域 si_ 双轨并存,DIF-29-4 三十期勘正口径)+分镜回填 image_id;失败:RefundGenError 502 信封+域内退款(:418-420,「导演分镜图生成失败自动退回(+{cost}):{err}」)。
5 输出与结果状态
{image_id, url, cost}(url=PathOf 落库口径)。
6 状态流转
shot.status=‘processing’(图链)→ 成功后回可生成视频态;重复生成追加 image_histories(历史链不截断)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 余额不足 | 402 | 统一信封 |
| provider 失败 | 502+已退款 | 「导演分镜图生成失败自动退回(+10):…」 |
| Mock 分支 | SVG 字节级移植产物(mockassets)同步 done | — |
8 权限与数据规则
authed + amw 属主面;扣费 type=‘image’;images.kind=‘storyboard’ 与 M2 拆解图同表隔离(task_id 前缀 dir_)。
F10.07 分镜视频生成(generate-video POST)——双表通路
对应 PRD:§4 M10 · POST /api/director3d/shots/:id/generate-video | 状态:实现 | 实现落点:director_handlers.go:182-201、generation/service_video.go:191-229(DirectorShotVideo)、repo.go:274-283(LinkShotVideo)| UI:Director3DView 分镜卡「生成视频」| 场景挂钩:E2E-DIR-01(video_id 回填断言面)
1 功能定义
单镜视频生成(V2 动态价形态):经 generation 域双表通路打 director_shots——ShotView.Tbl 显式字段是冻结 pseudoShot._tbl 鸭子字段的 Go 显式化(PRD ⑤)。
2 触发条件与前置状态
分镜属主(DirectorShotForUser JOIN director_projects,Tbl=‘director_shots’ generation/repo.go:273);余额不足 402。
3 输入与校验
无 body;duration/ratio/resolution 取自分镜行(resolution 空回「1080p」COALESCE,repo.go:266);videoCost(secs) clamp 1-15s(service_video.go:201-213)。
4 处理流程
TryDeduct videoCost「导演分镜视频 {secs}s(-{cost})」(V2 计费形态)→ SmartVideo 编排(poll/SSE video 事件照 M3 四帧)→ 成功 LinkShotVideo(tbl='director_shots',…):UPDATE video_id+status=‘video_done’+attempts+1(repo.go:274-283 按 Tbl 动态表名);失败退款「导演分镜视频失败自动退回(+{cost})」(Mock desc 参数化——PRD ⑮ 后半)。
5 输出与结果状态
{video_id, url, cost};分镜 video_url 随 F10.03 详情解析。
6 状态流转
status→‘video_done’(终态;取消走 M3 cancel 面 taskId=‘dir_’+pid 隔离)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| Link 失败 | videoFailFast 退款+500(service_video.go:316-318——DIF-29-22 failFast 口径) | — |
| Mock 分支 | 按 Tbl 打 director_shots 回填(冻结 Mock 分支 UPDATE shots 硬编码致导演分镜不回填——Go 按意图语义修,PRD ⑤ 登记) | — |
8 权限与数据规则
authed + amw 属主面;台账 type=‘video’;本节是全站唯一跨域写表通路(generation 域函数写 director 域表——经 Tbl 参数化而非 import,域边界守卫形态)。
F10.08 项目 ZIP 导出(:id/export GET)
对应 PRD:§4 M10 · GET /api/director3d/:id/export | 状态:实现 | 实现落点:director_handlers.go:202-213、export.go:26-162 | UI:Director3DView「导出项目」| 场景挂钩:无(E2E-EXP-01 为 M4 面)
1 功能定义
项目全量打包下载:六段 zip(project.json/episodes/*.txt/shots/EPxx.json/shots_all.json/images/EP{i}_shot{j}.png/videos/…svg)。
2 触发条件与前置状态
项目属主(403/404 同 F10.02);素材缺失静默跳过(store.Read 失败=冻结 fs.existsSync 守卫,export.go:118-133)。
3 输入与校验
无 body;文件名净化 [\\/:*?"<>|]→_(export.go:22)+slice20。
4 处理流程
zip.NewWriter 逐段 addZipEntry(:153-161):project.json(style 尾插+style_id 键保留 :38-49)→ episodes 文本 → shots 两形态 JSON → appendShotMedia 逐镜产物(store.Read,:118-133)→ 文件名 director-{pid前8}.zip(:86)→ zipRespond 二进制直出(application/zip+Content-Disposition)。
5 输出与结果状态
zip 字节流(PK 魔数——集成锚 integration_director_test.go:204-210 断言 :204 前缀)。
6 状态流转
零写面。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 分镜产物文件缺失 | 跳过该条目(zip 不中断) | — |
| 空项目 | 仅 project.json+空 episodes 段 | — |
8 权限与数据规则
authed + amw 属主面;导出含全部素材字节(无脱敏)。
F10.09 项目 ZIP 导入(director3d/import POST)
对应 PRD:§4 M10 · POST /api/director3d/import(up20)| 状态:实现 | 实现落点:director_handlers.go:53-79、export.go:164-234 | UI:Director3DView「导入项目」| 场景挂钩:无
1 功能定义
v1 格式 ZIP 导入(project.json+episodes/*.txt)重建项目。
2 触发条件与前置状态
缺 ZIP 400「缺少 ZIP 文件」(model.go:158);解析失败 400「ZIP 解析失败:{e.message}」(:159,前缀拼接);限额 20MB(importZipLimit handlers :18+路由 up20,PRD ⑭)。
3 输入与校验
multipart file 字段;meta 必需 project.json(缺省兜底形态 export.go:192-234);episodes 字典序排列。
4 处理流程
zip.NewReader(内存)→ project.json 解析(meta 缺省兜底)→ episodes/*.txt 按文件名字典序重建(标题「{原名}(导入)」后缀)→ SplitEpisodes 文本再切(1200 阈值——director.js:268 导入源)。
5 输出与结果状态
{id, episodes: n}(同 create 形态)。
6 状态流转
新项目 enabled 语义同 create;旧项目不受扰。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 缺文件 | 400 | 「缺少 ZIP 文件」 |
| 非 ZIP/损坏 | 400 | 「ZIP 解析失败:…」 |
| 无 project.json | meta 缺省兜底继续(quirk) | — |
8 权限与数据规则
authed + amw;导入产物属主=当前用户;不导入素材字节(v1 仅文本结构——images/videos 不在包规格)。
F10.10 资产 CRUD 与公共池导入(assets 四件)
对应 PRD:§4 M10 · GET/POST /api/director3d/:id/assets、DELETE /api/director3d/assets/:id、POST /api/director3d/:id/assets/import | 状态:实现 | 实现落点:director_handlers.go:214-243/:300-327、service_assets.go:44-118/:417-435 | UI:Director3DView L1 资产面(公共池
['asset-library']同 key)| 场景挂钩:无
1 功能定义
项目资产三组读写+资产库(asset_library 公共池)导入。
2 触发条件与前置状态
GET/POST 前置项目属主;DELETE 查在先恒 ok(service_assets.go:74-82);import 前置库资产存在 400「资产库资产不存在」(model.go:152)。
3 输入与校验
POST {name, style_id, face_style, script_text, kind}(handlers :36-42 复用)——name/kind 必填 400「名称与类型必填」(model.go:149);风格后缀拼接(service_assets.go:109-117 注「与 extract 前缀两源刻意不同」)。
4 处理流程
import:asset_library 行复制为项目 assets(task_id=‘dir_’+pid,公共池隔离靠前缀);DELETE 单行(级联 image 引用不动——image_id 保留孤儿兼容)。
5 输出与结果状态
列表=AssetRow+groups 三组尾插(service_assets.go:48-51 status 派生);AssetRow.StyleID/FaceStyle json:"-" 不出 wire(model.go:81-82)。
6 状态流转
queue_status 由 F10.12 写;status 派生(image_id 有无)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 删除不存在资产 | 200 恒 true(查在先不拦截——quirk) | — |
| 队列候选谓词 | image_id='' 代冻结 IS NULL(repo.go:451——MySQL NOT NULL DEFAULT ’’ 方言适配,PRD ⑮ 同族) | — |
8 权限与数据规则
authed + amw 属主面;公共池 asset_library 全站共享读(导入才落项目域)。
F10.11 资产生成(generate + batch-generate POST)
对应 PRD:§4 M10 · POST /api/director3d/assets/:id/generate、POST /api/director3d/:id/assets/batch-generate | 状态:实现 | 实现落点:director_handlers.go:244-271、service_assets.go:120-198 | UI:Director3DView 资产卡「生成」| 场景挂钩:无
1 功能定义
单个/批量资产立绘生成(ImageCost 计价);批量逐笔容错(失败回 queued 不整单失败)。
2 触发条件与前置状态
已生成资产入队 400「该资产已生成,无需入队」(model.go:150);batch:无 ids 且无待生成 400「没有待生成的资产」(:151)/有 ids 但全已生成 400「所选资产均已生成」(:150——双哨兵分叉 service_assets.go:163-167,二十续 A2 空数组方言收口)。
3 输入与校验
generate 无 body;batch {ids?:[]}——空数组=全量待生成(A2:MySQL IN () 500→Go 收口语义)。
4 处理流程
单发:TryDeduct「资产生成:{name}(-{cost})」→Generate→回填;失败退「资产生成:{name}失败自动退回(+{cost}):{err}」。批量:逐笔同上+失败 queue_status 回 queued 继续(:185-198)。
5 输出与结果状态
generate {image_id, url, cost};batch {generated: n, failed: m}(failed=剩余数语义)。
6 状态流转
queued→生成中→done(image_id 回填)/失败回 queued(可重试)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| batch 空数组 | 全量待生成语义(A2 收口) | — |
| 单笔失败 | 退费+回 queued+continue | — |
| mid-batch 余额耗尽 | 后续笔 402 整批中断(已生成不回滚) | — |
8 权限与数据规则
authed + amw 属主面(batch ids 逐个校验属主);扣费 type=‘image’。
F10.12 待生成队列(queue 二件)
对应 PRD:§4 M10 · POST /api/director3d/assets/:id/queue、GET /api/director3d/:id/queue | 状态:实现 | 实现落点:director_handlers.go:272-299、service_assets.go:200-246、repo.go:18-26/:451 | UI:Director3DView 队列面板 | 场景挂钩:无
1 功能定义
资产入队/队列视图(持久化 queue_status)——批量排程前置面。
2 触发条件与前置状态
入队前置:已生成 400「该资产已生成,无需入队」;action 归一(非 unqueue 一律按 queue,service_assets.go:223-225 quirk 照抄 fusion.js:105)。
3 输入与校验
{action} 任意字符串(归一语义);视图无分页。
4 处理流程
入队:queue_status=‘queued’;出队:=’’/置空;视图:QueueItemRow 七键投影(repo.go:18-26)双键排序。启动恢复由 recovery.requeueAssets 承接(service_assets.go:211-212 注——重启丢队列态回补)。
5 输出与结果状态
入队/出队恒 true;视图 queued 列表。
6 状态流转
queued ⇄ 空(unqueue);done 由 F10.11 写。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| action 未知值 | 按 queue 处理(归一 quirk) | — |
| 重启后 queued 行 | recovery 回补入队 | — |
8 权限与数据规则
authed + amw 属主面;零扣费(入队不扣,生成才扣)。
F10.13 融合生图与图片改创(fusion 二件 + remix)
对应 PRD:§4 M10 · POST/GET /api/director3d/:id/fusion、POST /api/director3d/:id/remix | 状态:实现 | 实现落点:director_handlers.go:328-371、service_assets.go:249-383 | UI:Director3DView 融合 tab | 场景挂钩:无
1 功能定义
双图融合生图(cost=3 硬编码)+融合历史列表+单图改创(remix,ImageCost 计价)。
2 触发条件与前置状态
fusion:画面描述必填 400「请填写画面描述」(model.go:154);remix:原图存在 400「原图不存在」(:156)+改创说明必填「请填写改创说明」(:157)。
3 输入与校验
fusion {image_id_a, image_id_b, description, scene_id?}——scene_id 空串→NULL 指针(sceneIDPtr service_assets.go:509-514,scene_id || null 冻结同形);remix {image_id, instructions}。
4 处理流程
fusion:const cost = 3 TryDeduct「融合生图(-3)」(service_assets.go:260 注「非 ImageCost——差异登记 PRD ⑦」)→ 双图 prompt 组合 → images(kind=fusion) → fusion_items 行(响应带 cost :310)。remix:ImageCost「图片改创(-{cost})」(:366-369)→ images(kind=remix)。
5 输出与结果状态
fusion create {id, url, cost};list=融合历史(LIMIT 面);remix {image_id, url, cost}。
6 状态流转
零状态列(同步完成)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 描述/说明空 | 400(扣费前) | 「请填写画面描述」/「请填写改创说明」 |
| 原图不存在 | 400 | 「原图不存在」 |
| fusion cost 调价 | 不随 settings(硬编码 3——PRD ⑦,与 ImageCost 双轨) | — |
8 权限与数据规则
authed + amw 属主面(image_id 引用校验属主);fusion_items.scene_id 可空外键语义(NULL=非场景绑定)。
F10.14 参考视频(ref-videos 三件)
对应 PRD:§4 M10 · GET/POST /api/director3d/:id/ref-videos、DELETE /api/director3d/ref-videos/:id | 状态:实现 | 实现落点:director_handlers.go:372-425、service_assets.go:385-435 | UI:Director3DView 参考视频区 | 场景挂钩:无
1 功能定义
项目参考视频上传(60MB 新档位)/列表/删除。
2 触发条件与前置状态
上传前置文件存在 400「缺少视频文件」(model.go:158 族)+扩展名白名单 400「仅支持 mp4/webm/mov」(:159);限额 60MB(refVideoLimit handlers :19+路由 up60 routes.go:208 新档位——PRD ⑭;handler 单文件双保险 500「File too large」:391-393)。
3 输入与校验
multipart file+name/note form tag(handlers :403-407——json tag 恒不命中,二十续 A1 修复锚);name/note 可空。
4 处理流程
store.Put 大文件流 → ref_videos 行(rv_ 族)→ 列表蛇形投影;DELETE 恒 ok 不查行数(service_assets.go:430-435 quirk 照抄 fusion.js:215-218)。
5 输出与结果状态
上传 {id, url, name};列表 rv 行数组;删除恒 true。
6 状态流转
零状态(上传即可用)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 扩展名非三白名单 | 400 | 「仅支持 mp4/webm/mov」 |
| 超 60MB | 路由 413/handler 500 双层 | 「File too large」(handler 层) |
| 删除不存在 | 200 恒 true(quirk) | — |
8 权限与数据规则
authed + amw 属主面;素材字节落 MinIO/local(files 目录),wire 出 url=PathOf 口径。
F10.15 一键转剧(convert/:eid POST)
对应 PRD:§4 M10 · POST /api/director3d/:id/convert/:eid | 状态:实现 | 实现落点:director_handlers.go:426-436、service_assets.go:440-481 | UI:Director3DView「一键转剧」| 场景挂钩:无
1 功能定义
分集文本一键转剧:flat=30 硬编码扣费→Mock 视频即弃(不落 videos 表)。
2 触发条件与前置状态
eid↔project 有关联校验(WHERE id AND project_id,service_assets.go:451-453——PRD ⑥ 的 convert 例外面:convert 是唯一校验关联的端点);余额不足 402。
3 输入与校验
无 body;空场次→意图语义:ep.title 单场(:471-481——冻结 :262-267 fallback 对象调 .split 恒 TypeError 500,Go 按意图修,PRD ⑧ 登记)。
4 处理流程
const flat = 30 TryDeduct type='video'「一键转剧《{ep.title}》(-30)」taskID=裸 p.ID(:454-456——无 dir_ 前缀,PRD ⑦;集成断言 integration_director_test.go:229/:235 钉死)→ MakeVideo 占位 SVG 落盘 → 不落 videos 表(service_assets.go:440-441 注——与 M3 视频链不同源,产物一次性)。
5 输出与结果状态
{url, cost}(url 一次性产物链接)。
6 状态流转
无任务行无轮询(同步伪完成形态——M4 同款语义,status 无从查证)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| eid 不属该项目 | 404(关联校验——convert 例外 PRD ⑥) | — |
| 空场次 | 单场兜底(意图语义 PRD ⑧) | — |
| 硬编码 30 | 不随 settings(PRD ⑦) | — |
8 权限与数据规则
authed + amw 属主面;台账 type=‘video’ task_id=裸 pid(credit-records 按 taskId 聚合时与 M3 视频同族——m04 对账注记关联面)。
册尾端点覆盖对账(28/28)
对账 backend/cmd/server/routes.go:203-238 亲数 28 条 ↔ 本册 F10.01~F10.15。口径脚注:routes.go:209/:223 注释「导演面 11(director.js)/融合面 17(fusion.js)」系冻结源文件归属计数(#1/#2 冻结在 fusion.js:221/:227 却排导演注释块内);物理分组=导演功能 13+融合功能 15,28 总数自洽。非本域同前缀:share(M6 组 :115,冻结 index.js:574——m06 F6.05)/review(M11 :266-267)勿误收。
| # | 端点(routes.go:line) | 节 | # | 端点(routes.go:line) | 节 |
|---|---|---|---|---|---|
| 1 | GET /director3d(:210) | F10.01 | 15 | POST /director3d/:id/assets(:225) | F10.10 |
| 2 | POST /director3d(:211) | F10.01 | 16 | POST /director3d/assets/:id/generate(:226) | F10.11 |
| 3 | POST /director3d/import(:212) | F10.09 | 17 | POST /director3d/:id/assets/batch-generate(:227) | F10.11 |
| 4 | DELETE /director3d/:id(:213) | F10.02 | 18 | POST /director3d/assets/:id/queue(:228) | F10.12 |
| 5 | GET /director3d/:id(:214) | F10.02 | 19 | GET /director3d/:id/queue(:229) | F10.12 |
| 6 | POST /director3d/:id/resplit(:215) | F10.02 | 20 | DELETE /director3d/assets/:id(:230) | F10.10 |
| 7 | GET /director3d/episodes/:eid(:216) | F10.03 | 21 | POST /director3d/:id/assets/import(:231) | F10.10 |
| 8 | POST /director3d/:id/episodes/:eid/extract-assets(:217) | F10.04 | 22 | POST /director3d/:id/fusion(:232) | F10.13 |
| 9 | POST /director3d/:id/episodes/:eid/shots(:218) | F10.05 | 23 | GET /director3d/:id/fusion(:233) | F10.13 |
| 10 | PUT /director3d/shots/:id(:219) | F10.05 | 24 | POST /director3d/:id/remix(:234) | F10.13 |
| 11 | POST /director3d/shots/:id/generate-image(:220) | F10.06 | 25 | GET /director3d/:id/ref-videos(:235) | F10.14 |
| 12 | POST /director3d/shots/:id/generate-video(:221) | F10.07 | 26 | POST /director3d/:id/ref-videos(:236) | F10.14 |
| 13 | GET /director3d/:id/export(:222) | F10.08 | 27 | DELETE /director3d/ref-videos/:id(:237) | F10.14 |
| 14 | GET /director3d/:id/assets(:224) | F10.10 | 28 | POST /director3d/:id/convert/:eid(:238) | F10.15 |
差异纪律:PRD §4 M10 ①~⑮ 全引用不重立;二十续 A1/A2/A3 引 PRD v1.12 变更行;quirk(双方一致 as-is)节内登记不立 DIF——本册零新立 DIF(差异面全在 PRD 已登记集合;image_id=’’ 谓词引 ⑮ 族)。
4.3 - FSD M11 · 评估与工具
← 总则 | 附录 → | 上游:PRD §4 M11 实现落点:
backend/internal/domain/evaluation/(model/service〔workbench+watchVideo〕/service_eval〔评估流水线〕/service_review〔审片双入口〕/service_viral/repo)+cmd/server/workbench_handlers.go+cmd/server/evaluation_handlers.go+cmd/server/routes.go:239-267(挂载,审片 4 条 :264-267 物理居中)。事实基准=Go 代码;冻结版qmtv/server/workbench.js/evaluation.js/index.js:490-513(viral)仅差异对拍参照。 域内要点:评估冻结-结算是全站唯一 TypeReserve 台账面(总则 §1 词条的展开落点);审片双入口 404 文案分叉(classic ’task not found’ vs director ’not found’);审片面零 SSE 零通知;watchVideo 状态丢失不 emit;wire 全蛇形;PRD §4 M11 已登记差异 ①~⑭(prd-qmtv-v1.md:216)引用不重立。上传档位 up80(工作台)/up10(评估)。
M11 评估与工具
14 端点全量(workbench 2+审片 4+eval 6+viral 2)。三扣费面三制式:工作台 TOOLS 固定价、审片 10(空分镜先退后拒)、评估 TypeReserve 冻结-结算、viral 5——四制式互不同源(quirk 富矿域)。
F11.01 工作台工具提交(workbench/tasks POST)
对应 PRD:§4 M11 · POST /api/workbench/tasks | 状态:实现 | 实现落点:workbench_handlers.go:18-54、service.go:141-221 | UI:WorkbenchView(E2E-GEN-02 已实现——toast「去字幕完成」;confirm 仅 repaint 空描述分支)
1 功能定义
视频工作台三工具(一键转绘 20/去字幕 10/转高清 15)提交:扣费→异步处理→SSE workbench done。
2 触发条件与前置状态
登录态;tool key 白名单(TOOLS map);余额不足 402;双载荷(multipart 视频/JSON 文本,PRD ⑩)。
3 输入与校验
{tool, video_id?/描述}——repaint 需描述(前端 confirm 分支对应);taskId 无=空串(台账「工作台·X(-N)」service.go:176-178 逐字,冻结 workbench.js:52)。
4 处理流程
TryDeduct(固定价,settings 无此键)→ INSERT workbench_tasks(status=‘processing’)→ 异步 goroutine(提交段同步返回)→ watchVideo 编排(F11.02)。
5 输出与结果状态
{id, status:'processing'}(wb_ 行投影);列表窄投影不含 params 全文(model.go json:"-" 面:OutPath/VideoStatus JOIN 伴列)。
6 状态流转
processing→done(SSE workbench done 帧)/failed(视频链失败联动);「任务状态丢失」为终态旁支(不 emit,F11.02 §7)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| tool 非三值 | 400 | TOOLS 键校验 |
| 余额不足 | 402 | 统一信封 |
| 提交后进程重启 | RecoverStuck 回队(PRD ⑨,F11.02 §4) | — |
8 权限与数据规则
authed + amw 属主面;up80 档(workbench.js:12 multer 80MB 对齐,handler 单文件双保险 500「File too large」:35-37)。
F11.02 工作台列表/轮询/启动恢复(workbench/tasks GET)
对应 PRD:§4 M11 · GET /api/workbench/tasks | 状态:实现 | 实现落点:workbench_handlers.go:57-66、service.go:225-304 | UI:WorkbenchView 列表+轮询 | 场景挂钩:E2E-GEN-02(间接)
1 功能定义
本人工作台任务列表 + watchVideo 轮询状态机 + 服务重启悬挂恢复。
2 触发条件与前置状态
列表登录态(按 user_id);watchVideo 由 F11.01 提交后 goroutine 内启动(非 HTTP 端点)。
3 输入与校验
无参数(全量本人任务,无分页——照冻结 :78-123)。
4 处理流程
watchVideo(service.go:228-260):首 tick 3s/步进 5s/上限 180 次(model.go:117-122 可注入常量)→ VideoStatusOf 查 M3 videos 行 → done:SetWorkbenchDone+hub.Emit workbench done;视频 failed:SetWorkbenchWatchFailed 联动;状态丢失(!found 或超次):置失败后直接 return 不 emit(:238-241——PRD ⑥:冻结 :84 同分支无 bus.emit)。 RecoverStuck:启动扫 processing 悬挂行→重挂 watchVideo(PRD ⑨,二十续迁移面)。
5 输出与结果状态
列表行+status(含 done/failed/「任务状态丢失」文案面)。
6 状态流转
与 F11.01 §6 同一张状态机(本节是驱动器)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 轮询超 180 次 | 置「任务状态丢失」无 SSE 帧(quirk 照抄——前端轮询兜底可见) | — |
| 超时不退款 | 扣费不回(PRD ⑥ 已登记) | — |
| goroutine panic | recover 吞(service.go:227——防崩保护,watchVideo 假漏覆写测试教训面) | — |
8 权限与数据规则
authed + amw;轮询消费 M3 videos 表跨域读(VideoStatusOf 窄投影三列)。
F11.03 智能审片·写(review 双入口 POST)
对应 PRD:§4 M11 · POST /api/tasks/:id/review、POST /api/director3d/:id/review | 状态:实现 | 实现落点:workbench_handlers.go:69-92、service_review.go:21-73 | UI:DashboardView/VideoView 审片(classic)+ Director3DView 审片(director)| 场景挂钩:无
1 功能定义
智能审片提交:按 scope 拉分镜→LLM/Mock 逐镜评审→review_reports 落行。双入口同表同逻辑、404 文案分叉。
2 触发条件与前置状态
classic:task 存在+属主,404「task not found」(service_review.go:24);director:404「not found」(:34)——两哨兵不同字(model.go:224/:227,PRD ⑦);空分镜:先退台账再 400「审片退回:无分镜(+10)」(:52-58,Refund 唯一口 nil claim)。
3 输入与校验
无 body(scope 由服务端定);审片费 10 固定(402 走统一信封)。
4 处理流程
TryDeduct「智能审片(-10)」→ 拉分镜(classic 走 shots/director 走 director_shots——跨域共表 COALESCE 可空列读,PRD ⑪ 炸点修复面)→ LLM 截 40 条(firstN :95)→ mockReview:阈值 0.22/0.5/0.7 三档 issue(high/mid/low,:140-146)+维度分 78+rand20,主体一致性 −6,clamp [55,98](:157-161)→ review_reports 落行(scope/target_id 归属)。
5 输出与结果状态
{id}(report 行 id);详情走 F11.04。
6 状态流转
同步完成(review_reports 一次落定,无 processing 态)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| classic 任务不存在 | 404 | 「task not found」 |
| director 项目不存在 | 404 | 「not found」(文案分叉勿统一) |
| 无分镜 | 先退 10 再 400(台账+拒绝双写) | 「审片退回:无分镜(+10)」 |
8 权限与数据规则
authed + amw 属主面;零 SSE 零通知(全文件 grep Emit/InsertNotification 零命中——PRD ⑦,唯一不发通知的写路径);扣费 type=‘review’。
F11.04 智能审片·读(review 双入口 GET)
对应 PRD:§4 M11 · GET /api/tasks/:id/review、GET /api/director3d/:id/review | 状态:实现 | 实现落点:workbench_handlers.go:81-116、service_review.go:76-88 | UI:同 F11.03 读侧 | 场景挂钩:无
1 功能定义
审片报告读取:按 target 取最新报告(含逐镜 issues/维度分/总分)。
2 触发条件与前置状态
属主校验同写侧(双 404 文案分叉同 F11.03 §2);无报告返回 null 形态(非 404——照冻结 :198-203/:220-225)。
3 输入与校验
路径 :id(classic=task id / director=project id)。
4 处理流程
review_reports 按 target_id DESC 取首行 → report JSON 反序列化下发。
5 输出与结果状态
{report: {...}|null};issues 数组键形照 mockReview 产出(shot_no/level/message)。
6 状态流转
零写面。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 从未审过 | report=null(200) | — |
| 越权 | 404 双文案分叉 | 同 F11.03 |
8 权限与数据规则
authed + amw 属主面;报告 JSON 全文下发(无脱敏)。
F11.05 评估配置与上传(evaluations/config + upload)
对应 PRD:§4 M11 · GET /api/evaluations/config、POST /api/evaluations/upload(up10)| 状态:实现 | 实现落点:evaluation_handlers.go:17-53、service_eval.go:28-43/:47-90/:521-590 | UI:EvaluationView 配置区+上传 | 场景挂钩:无
1 功能定义
评估价目配置读面(quick/deep 双价)+ 剧本文件上传解析(txt/docx、GBK 回退)。
2 触发条件与前置状态
config 登录态;upload up10 档(原 studio cover 档复用)。
3 输入与校验
config 无参;upload multipart file——.txt/.docx 白名单;costOf settings 0 值回退默认(service_eval.go:38-42 err != nil || n == 0 → def,PRD ①;quick 默认 100/deep 300)。
4 处理流程
- config:两价+引擎名——engine『规则(Mock)』字面(:48-51,与 F11.06 POST 面『规则』两字面不同源,PRD ②)。
- upload:GBK 判定(ToValidUTF8→U+FFFD 计数>max(2, 0.2%)→GB18030 解码 :521-534,M9 同族)→ .docx 走 zip 解包+XML 五实体反转义(&/</>/"/&#N;,:537-590)→ 纯文本回传。
5 输出与结果状态
config {quick: n, deep: n, engine: str};upload {text, name, chars} 形态(rune 计数)。
6 状态流转
零写面(upload 不落库——解析产物仅回显,落库在 F11.06 create)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| settings 价=0 或解析失败 | 回默认 100/300(0 不生效 quirk,PRD ①) | — |
| docx 损坏 | 解包错误透传 500 族 | — |
8 权限与数据规则
authed + amw;价键 eval_cost_quick/eval_cost_deep(管理台计费配置可调——标「可调」族)。
F11.06 评估项目与流水线(evaluations 三件+runEval)
对应 PRD:§4 M11 · POST/GET /api/evaluations、GET /api/evaluations/:id | 状态:实现 | 实现落点:evaluation_handlers.go:54-101、service_eval.go:93-153/:226-321 | UI:EvaluationView 列表+进度轮询 | 场景挂钩:无(SSE eval 三态帧:stage/done/failed)
1 功能定义
评估项目创建(TypeReserve 冻结扣费)→ 异步流水线(五 stage 推进+SSE)→ 终态结算。
2 触发条件与前置状态
create:script_text 必填;mode ∑ {quick, deep}——非法值静默归 quick 无 400(service_eval.go:97-100,PRD ③);余额不足 402。
3 输入与校验
{name, script_text, mode}——name 缺省「未命名评估」族;script_text 落库截断(与 upload 解析面衔接)。
4 处理流程
TryDeduct type='reserve'「剧本快速/深度评估冻结(-N,完成后结算)」taskID=id(:104-106 冻结 :252-253 逐字)→ INSERT eval_projects(status=‘running’)→ 异步 goRun(:226 起,注入点便于测试):五 stage 推进(解析/拆解/维度打分/汇总/出报告,SSE eval stage 帧逐发)→ Mock 确定性打分 (v*7919)%40(:398,冻结 :56 镜像)→ done:SetEvalDone(无台账动作——冻结即终态;TypeReserve 台账仅失败面全额退款消化)+SSE done;失败:全额退款「评估失败自动退回(+N)」+SSE failed+通知「评估失败:《name》…N 积分已退回」(evalFail :311-321,错误截 100)。
5 输出与结果状态
create {id};list=EvalListRow 定列七投影(model.go:173-181,Report json:"-" 不入列表);:id=全行+report(终态才有)。
6 状态流转
running→done/failed(SSE eval 帧+前端 3s 轮询双通道——E1 轮询机保留手管先例);finished_at UTC 形态(PRD ④)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| mode 乱值 | 归 quick(静默) | — |
| 流水线任一 stage 失败 | 全额退款+SSE failed+通知 | 「评估失败自动退回(+300)」 |
| Mock sleep | 1100/1500/900ms 三段(:287-291——Mock 行为是契约) | — |
| 模板缺键 | <nil> 透传(PRD ⑭) | — |
8 权限与数据规则
authed + amw 属主面;TypeReserve 是全站唯一冻结-结算制(先冻结后按结果结算/失败全退——总则 §1 词条展开);evalLLM 通道已接线(PRD ⑤,二十一期⑦)。
F11.07 评估报告下载(evaluations/:id/report GET)
对应 PRD:§4 M11 · GET /api/evaluations/:id/report | 状态:实现 | 实现落点:evaluation_handlers.go:102-114、service_eval.go:156-222 | UI:EvaluationView「下载报告」| 场景挂钩:无
1 功能定义
评估报告 GAE 模板渲染下载(docx 兼容 HTML 容器)。
2 触发条件与前置状态
项目存在+属主;status 非 done 时 report 缺失→按冻结形态返回空/错误面。
3 输入与校验
路径 :id;无 body。
4 处理流程
report JSON → GAE 模板变量填充(七维表/总分/建议段)→ HTML 全文响应(前端 blob 下载——E1 保留 fetch+blob 先例)。
5 输出与结果状态
text/html 全文(非 JSON 信封——下载面直出);engine 字面「规则」(POST 面字面——PRD ② 双字面对照 F11.05)。
6 状态流转
零写面。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 未完成即取 | 空报告形态 | — |
| 越权 | 404 | — |
8 权限与数据规则
authed + amw 属主面;报告含全维度明文。
F11.08 爆款分析(viral-analysis 二件)
对应 PRD:§4 M11 · GET/POST /api/viral-analysis | 状态:实现 | 实现落点:evaluation_handlers.go:115-138、service_viral.go:16-67、repo.go:397 | UI:ViralAnalysisView | 场景挂钩:无
1 功能定义
爆款标题/文案四维建议生成(math/rand Mock):单次扣 5 积分,历史保留最近 20 条。
2 触发条件与前置状态
create:title 必填(空→400 族);余额不足 402。
3 输入与校验
{title};title 截 255(slice :56,PRD ⑧ 二十一期 B2 补登)。
4 处理流程
TryDeduct CostViral=5(billing.go:15)→ math/rand 四维 advice(score=60+Intn(35)、comment/emoji/duty 三键随机池,service_viral.go:44-50——math/rand 非确定性 Mock,PRD ⑧;与 eval 7919 确定性不同制)→ viral_analyses 落行(五列 INSERT)→ 响应。
5 输出与结果状态
create {id, advice:{score, comment, emoji, duty}};list=本人最近 LIMIT 20(repo.go:397),Result/Status json:"-"(model.go json:"-" 面)。
6 状态流转
零状态(同步完成落行)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| title 超 255 | 截断(不报错) | — |
| 历史超 20 | 旧条滚出(LIMIT 查询面) | — |
8 权限与数据规则
authed + amw 属主面;扣费 type=‘viral’(TypeViral 独立常量——credit-records 单列归组)。
册尾端点覆盖对账(14/14)
对账 backend/cmd/server/routes.go:239-267 亲数 14 条 ↔ 本册 F11.01~F11.08(审片 4 条 :264-267 物理居 M11 段,:259-262 为 M13 spark 段——紧邻勿混):
| # | 端点(routes.go:line) | 节 | # | 端点(routes.go:line) | 节 |
|---|---|---|---|---|---|
| 1 | POST /workbench/tasks(:247) | F11.01 | 8 | GET /evaluations/:id/report(:255) | F11.07 |
| 2 | GET /workbench/tasks(:248) | F11.02 | 9 | GET /viral-analysis(:257) | F11.08 |
| 3 | GET /evaluations/config(:250) | F11.05 | 10 | POST /viral-analysis(:258) | F11.08 |
| 4 | POST /evaluations/upload(:251) | F11.05 | 11 | POST /tasks/:id/review(:264) | F11.03 |
| 5 | GET /evaluations(:252) | F11.06 | 12 | GET /tasks/:id/review(:265) | F11.04 |
| 6 | GET /evaluations/:id(:253) | F11.06 | 13 | POST /director3d/:id/review(:266) | F11.03 |
| 7 | POST /evaluations(:254) | F11.06 | 14 | GET /director3d/:id/review(:267) | F11.04 |
(:249/:256/:259-263 为注释/空行/M13 spark 段邻接行,非端点。)
差异纪律:PRD §4 M11 ①~⑭ 全引用不重立;本册零新立 DIF(goroutine recover/三注入点系测试基建形态非行为差异,节内注记);集成锚第十一链 EvaluationChain/第十二链 WorkbenchNotifyChain 在位(backend/integration_test.go)。
4.4 - FSD M12 · 管理后台
← 总则 | 附录 → | 上游:PRD §4 M12 实现落点:
backend/internal/domain/admin/(model 276/repo 476/service 332/service_billing 416/service_prompts 250 单包多文件+三测试件)+cmd/server/admin_handlers.go(562 行 39 handler)+cmd/server/routes.go:268-318(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js(admin 段 app.* 直挂非 router,19−withdrawals 2=17)+llmroutes.js(18)+routes2.js(admin 4)仅差异对拍参照。 域内要点:39 条全部 amw+adminMw 双挂(403forbidden,gateway/auth.go:133-141=冻结 adminRequired 逐字);前端消费面≠端点族——AdminView 14 tab(AdminView.vue:667-682)中 submissions/withdrawals/scripts 三 tab 消费 M9/M7 域端点,本册 39 条按后端功能族 AH 组聚合;404 八态八哨兵刻意不同源(model.go:16-36:用户调整英文 “not found”/密码重置中文「用户不存在」/org approve 英文/指令 DELETE 英文/提示词删「版本不存在」/接入「接入不存在」/release「任务不存在或不是失败态」/重拆「任务不存在」);bind 失败统一 400「请求体格式错误」(与 m08 裸传 return err 两域不同制);⑨(prd-qmtv-v1.md:222)引用不重立。api_keyjson:"-"masked-only(二十一期②收敛,PRD ④);GetActivePrompt(整行+split 三级)↔activePromptContent(content-only 单级)两份同语义并存(二十二期 C1 裁决 a,task_assembly.go:188-189 注释「两处改动须同步对拍」)。PRD §4 M12 已登记差异 ①
M12 管理后台
39 端点全量(冻结三文件 41 物理 − withdrawals 2 归 M7;对账脚注见册尾)。静态段优先三处:credit-records/export、instruction-versions/active、prompts/upload 与 :id 参数段并存——echo radix 静态子节点恒先匹配(routes.go:270-272 注)。
F12.01 概览与运行时观测(summary/runtime)
对应 PRD:§4 M12 · GET /api/admin/summary、GET /api/admin/runtime/tasks | 状态:实现 | 实现落点:admin_handlers.go:19-35/:127-135、service.go:63-65/:236-246、repo.go:91-109/:215-229 | UI:AdminView.vue(概览 tab+运行时面板 30s 轮询)| 场景挂钩:E2E-ADM-01(✅ 只读巡检断言 .stat≥5);AdminChain 集成锚
1 功能定义
全站六计数概览+运行中视频/拆解双列表观测(前端 30s 轮询面)。
2 触发条件与前置状态
双挂+requireDB;零参数零 body(无 bind 路径)。
3 输入与校验
无输入。
4 处理流程
summary:六条独立 SELECT 顺序执行无事务(repo.go:91-109=冻结同形)。runtime:ListRuntimeVideos(status IN ('processing') LIMIT 50)+ListRuntimeSplits(split_status='processing' LIMIT 50)。
5 输出与结果状态
summary 顶层键序 users,tasks,images,videos,credits_granted,credits_consumed(model.go:63-70=冻结 :641-648 展开序);runtime 键序 videos,splits,videos 行键 id,status,seconds,resolution,created_at,email,title(title *string 可空)、splits 行键 id,title,created_at,email。
6 状态流转
只读;不订阅 SSE(M12 全程无 bus.emit——repo.go:252-253 注,与 m08 recharge SSE 对照面)。
7 边界与异常
无哨兵路径;DB 错误 → errRespond default 500。
8 权限与数据规则
quirk:六计数非事务一致性(计数间可撕裂,冻结同形照抄);credits_granted/consumed 以 amount>0/amount<0 符号二分而非 type 字段。
F12.02 用户列表与合并 mutation(users GET/PUT)
对应 PRD:§4 M12 · GET、PUT /api/admin/users(/:id) | 状态:实现 | 实现落点:admin_handlers.go:30-58、service.go:67-98、repo.go:113-153 | UI:AdminView.vue(用户管理 tab——adjustCredits/toggleUserStatus/setAdmin 合并 mutation)| 场景挂钩:AdminChain;E2E-ADM-01(用户行≥3)
1 功能定义
8 列投影全量用户列表+credits 差额调整/status/role 更新的合并 mutation。
2 触发条件与前置状态
双挂+requireDB;PUT 经 GetUserCredits 无行 → 404 英文 “not found”(ErrAdjustNotFound——与 F12.03 中文 404 刻意不同源)。
3 输入与校验
GET 零参数。PUT body credits *int64(指针透传=冻结 credits != null 判空)+status/role string(空串跳过 truthy 才更新);bind 失败 → 400「请求体格式错误」。
4 处理流程
AdjustUser(service.go:74-98):①GetUserCredits ErrNoRows→404;②delta != nil → credits.Add(u.ID, float64(*delta)-u.Credits, admin_adjust, "管理员调整积分", "")——Δ=req.credits−u.credits 现值差;③status/role 各自独立 UPDATE 无事务(users 无 updated_at 列不写=冻结同形)。ErrNoUser 吞掉(PRD ③——冻结 addCredits 静默 no-op)。
5 输出与结果状态
GET 行键序 id,email,nickname,role,credits,status,referral_code,created_at(referral_code *string NULL→null);PUT data:true。无分页无搜索(前端 filteredUsers 客户端过滤,repo.go:113)。
6 状态流转
users.status/role 直改;credits 经 credits.Add 唯一口落 credit_records(type=admin_adjust)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 用户不存在 | 404 | “not found”(英文——八态之一) |
| ErrNoUser(并发删除窗口) | 200 | 吞掉静默(PRD ③) |
| bind 失败 | 400 | 「请求体格式错误」 |
8 权限与数据规则
quirk:差额调整语义(前端传绝对值后端算 Δ);跨域共表 users 本域持有写。
F12.03 密码重置(users/:id/password POST)
对应 PRD:§4 M12 · POST /api/admin/users/:id/password | 状态:实现 | 实现落点:admin_handlers.go:60-76、service.go:100-119、repo.go:131-159 | UI:AdminView.vue(用户管理 tab resetPassword)| 场景挂钩:AdminChain
1 功能定义
管理员重置用户密码(校验序 404→400 十六续 A1 修序)。
2 触发条件与前置状态
双挂+requireDB;UserExists 不存在 → 中文 404「用户不存在」(ErrUserNotFoundCN——八态之二)。
3 输入与校验
body password string;bind 失败 → 400「请求体格式错误」;len(password) < 8 → 400「密码至少 8 位」(冻结 index.js:143 逐字)。先存在后长度(顺序对齐冻结 :140-142,service.go:100-102 注「十六续 A1 修序」)。
4 处理流程
UserExists → 长度校验 → crypto.HashPassword → UpdateUserPassword 单列覆盖(repo.go:156-159)。
5 输出与结果状态
data:true。
6 状态流转
users.password_hash 覆写;refresh_tokens 不动(不撤销 refreshToken=冻结语义 quirk,repo.go:155-156)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 用户不存在 | 404 | 「用户不存在」(中文——与 F12.02 英文 404 分叉) |
| 密码 <8 位 | 400 | 「密码至少 8 位」 |
8 权限与数据规则
quirk:len() 字节序无 trim(冻结 String(req.body.password||'') 空串→len 0→400 同效);旧 refreshToken 仍有效。
F12.04 发放流水与发放(credit-grant-records GET/POST)
对应 PRD:§4 M12 · GET、POST /api/admin/credit-grant-records | 状态:实现 | 实现落点:admin_handlers.go:79-109、service.go:126-141、repo.go:162-175 | UI:AdminView.vue(积分 tab doGrant——全页唯一跨分区联动 grantRecords+users 双刷)| 场景挂钩:AdminChain
1 功能定义
发放流水列表(amount>0 跨分区联动)+向用户发放积分+通知。
2 触发条件与前置状态
双挂+requireDB;POST userID=="" || amount==0 → 400「参数错误」(冻结 :667 !user_id || !amount)。
3 输入与校验
POST body user_id/amount int64/description(缺省「管理员发放积分」);bind 失败 → 400「请求体格式错误」。
4 处理流程
credits.Add(userID, amount, admin_grant, desc, "")——task_id 空(冻结 4 参调用 :668),ErrNoUser 吞掉(冻结静默但通知照发 service.go:126-127 注)→ InsertNotification:title「积分到账」+content 管理员为您发放 %d 积分:%s(冻结 :669 逐字)。
5 输出与结果状态
GET 行键序 id,user_id,task_id,amount,balance_after,type,description,created_at,email,nickname(task_id *string NULL 透传、amount float64——0004 DECIMAL);LIMIT 200;无 type 过滤仅 amount>0(quirk:充值 credit/admin_grant/admin_adjust/release 退款全部入「发放流水」列表——跨分区联动面)。POST data:true。
6 状态流转
credit_records 新行 type=admin_grant + notifications 新行。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| user_id 空或 amount=0 | 400 | 「参数错误」 |
| 用户不存在 | 200 | ok:true 且通知照插(notifications 无 FK 阻断——PRD ③ 吞形) |
8 权限与数据规则
quirk:用户不存在仍 ok:true 通知照发(通知 userId 为不存在 id=冻结 :669 无条件执行同形)。
F12.05 流水导出 CSV(credit-records/export GET)
对应 PRD:§4 M12 · GET /api/admin/credit-records/export | 状态:实现 | 实现落点:admin_handlers.go:111-124、service.go:202-233、repo.go:178-182 | UI:AdminView.vue(积分 tab 导出按钮 fetch+blob)| 场景挂钩:无专项 e2e(AdminChain);静态段优先第一处(routes.go:270-272 注)
1 功能定义
全量流水 CSV 直出(Excel 可开)——非 JSON 信封端点。
2 触发条件与前置状态
双挂+requireDB;零参数零 body。
3 输入与校验
无输入。
4 处理流程
ListExportRows(7 列 JOIN DESC LIMIT 10000)→ "" BOM → 头行 时间,用户邮箱,昵称,类型,事项,积分,余额 → 每行 7 字段全量引号包裹("→"" 转义)+CRLF 分隔 → 时间 2006-01-02 15:04:05、数值 FormatFloat。handler:Content-Type text/csv; charset=utf-8 + Content-Disposition attachment + WriteHeader(200) 直写。
5 输出与结果状态
CSV 文本流(列序 created_at,email,nickname,type,description,amount,balance_after);LIMIT 10000 静默截断无溢出标记。
6 状态流转
只读。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| DB 错误 | 500 | JSON 信封(错误路径与成功路径信封分裂——admin_handlers.go:116-118) |
8 权限与数据规则
quirk:全字段强制双引号包裹(冻结 :196-197 同形);BOM+CRLF;10000 截断静默。
F12.06 生成审计与人工释放(failures/release)
对应 PRD:§4 M12 · GET /api/admin/generation-audit/failures、POST …/videos/:id/release | 状态:实现 | 实现落点:admin_handlers.go:140-159、service.go:146-184、repo.go:187-212+generation/repo.go:214-222 | UI:AdminView.vue(任务审计 tab)| 场景挂钩:AdminChain(47=7+5×8 videoCost 联动断言)
1 功能定义
失败视频+失败拆解双列表观测+失败视频人工退款释放(CAS 认领制)。
2 触发条件与前置状态
双挂+requireDB;release 仅路径 :id 零 body;FailedVideoByID(窄投影 5 列——error TEXT NULL 不进投影防 scan 坑,repo.go:203-204 注)ErrNoRows → 404。
3 输入与校验
GET 零参数;release 无 body 无 bind。
4 处理流程
failures:ListFailedVideos(status='failed' LIMIT 100,LEFT JOIN tasks)+ListFailedSplits(status='failed' OR split_status='failed' LIMIT 100)。release:①查行 404 → ②ClaimManualRelease(UPDATE videos SET refunded=1, error=CONCAT(COALESCE(error,''),'(已人工退款)') WHERE id=? AND refunded=0——CAS 认领+error 追加一步)affected=0 → 400「该任务已自动退款」 → ③cost=billing.VideoCost(secs)(secs<=0 归 5=冻结 seconds||5) → ④credits.Refund type=release desc「人工释放:失败视频退款(+%d)」。冻结序「查→检查 refunded→加币→置位」有并发双退窗,Go 认领前置收口(service.go:158-160 注,PRD ⑥)。
5 输出与结果状态
failures 键序 videos,splits;videos 行键 id,error,seconds,resolution,attempts,refunded,created_at,email,task_title,user_id(error/task_title *string NULL→null);splits 行键 id,title,error,created_at,email(tasks.error 已 0002 收紧 NOT NULL)。release {"refunded": cost}。
6 状态流转
videos:failed → refunded=1 + error 尾缀追加;credit_records type=release 行;无通知。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非 failed 态/不存在 | 404 | 「任务不存在或不是失败态」(八态之三) |
| CAS affected=0(已退) | 400 | 「该任务已自动退款,无需重复处理」(冻结 :173 逐字——Go 由 CAS 同义认领) |
8 权限与数据规则
跨域共表——videos 写归 generation(ClaimManualRelease 窄接口 service.go:40-42);退款额随 credit-settings 写侧联动(改 video_pull 后生效——集成断言 47=7+5×8)。
F12.07 重拆(retry-split POST)
对应 PRD:§4 M12 · POST /api/admin/generation-audit/tasks/:id/retry-split | 状态:实现 | 实现落点:admin_handlers.go:162-169、service.go:188-197、task/service.go:469-485 | UI:AdminView.vue(任务审计 tab)| 场景挂钩:AdminChain
1 功能定义
任务清分镜后 Mock 重拆(跨域共表写归 task 域 AdminRetrySplit)。
2 触发条件与前置状态
双挂+requireDB;仅路径 :id 零 body;AdminTaskByID ErrNoRows → 404。
3 输入与校验
无 body 无 bind。
4 处理流程
AdminRetrySplit(task/service.go:469-485):①AdminTaskByID → ②ClearShotImagesByTask(不删根资产=冻结无此步,重复重拆根资产重复系冻结原样,PRD ⑥)→ ③DeleteShotsByTask → ④fillWithMock 直插 → ⑤RecoverSplitDone(status='complete', split_status='done'=冻结 :187)。
5 输出与结果状态
data:true。
6 状态流转
tasks 任意态 → complete/done;shots+shot_images 销毁重建;assets 根资产不动。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务不存在 | 404 | 「任务不存在」(八态之四,ErrTaskNoExist——冻结 index.js:182) |
| handler 层 | 200 | 恒 ok(service 返 nil 即 OK,admin_handlers.go:168) |
8 权限与数据规则
tasks/shots/shot_images 共表写归 task 域(SplitRefiller 窄接口,TECH.md §2.1 共表登记);无用户属主校验(admin 全权=冻结)。
F12.08 公告管理(announcements 三件)
对应 PRD:§4 M12 · GET、POST /api/admin/announcements、PUT /api/admin/announcements/:id | 状态:实现 | 实现落点:admin_handlers.go:174-220、service.go:257-281+325-332、repo.go:234-257 | UI:AdminView.vue(公告 tab createAnn——busy quirk 同步复位照抄勿修)| 场景挂钩:AdminChain(扇出区间断言抗并发)
1 功能定义
公告 CRUD 面(读/建+全量扇出/toggle 置位——无 title/content 编辑端点,冻结同构)。
2 触发条件与前置状态
三处双挂+requireDB;POST title=="" || content=="" → 400「标题和内容必填」;PUT 无存在性校验(不存在 id 也 ok:true)。
3 输入与校验
POST body title/content(bind 失败 → 400「请求体格式错误」);PUT body active bool。
4 处理流程
CreateAnnouncement:an_ id → InsertAnnouncement 三列(active 落 DDL 默认 1)→ ListUserIDs 全量 → 逐用户 INSERT notification 扇出:title「新公告:」+title、content truncateRunes(content, 100)(rune 截断=冻结 content.slice(0,100) 等价)——循环内任一失败即中断返回 err(部分扇出无回滚=冻结 forEach 同形)。ToggleAnnouncement:单列 UPDATE 恒 ok(repo.go:246 注)。
5 输出与结果状态
GET 行键序 id,title,content,active,created_at;POST {"id"};PUT true。无 LIMIT。
6 状态流转
announcements.active 布尔位;notifications 全量扇出行。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| title/content 空 | 400 | 「标题和内容必填」 |
| PUT 不存在 id | 200 | 恒 true 静默 0 行 |
8 权限与数据规则
quirk:content 截 100 rune 只作用于通知、公告正文存全量;active wire bool(vs 冻结 1/0——DIF-31-3);公开读镜像归 M5(routes.go:102 pub 无 amw)。
F12.09 组织审批(organization-applications 三件)
对应 PRD:§4 M12 · GET、POST /api/admin/organization-applications(/:id/approve|/:id/reject) | 状态:实现 | 实现落点:admin_handlers.go:222-254、service.go:292-323、repo.go:262-301 | UI:AdminView.vue(orgAppReview tab——approve/reject 合并 mutation)| 场景挂钩:AdminChain(reject 幽灵 id 恒 ok 断言)
1 功能定义
组织入驻申请列表+审批(approve 建 org 四写+通知)+驳回(无条件恒 ok)。
2 触发条件与前置状态
三处双挂+requireDB;三端点均仅路径 :id 零 body 无 bind;approve 仅 pending 命中否则英文 404(WHERE id=? AND status='pending' 窄投影 3 列——reason TEXT NULL 不进投影)。
3 输入与校验
无输入。
4 处理流程
approve 四写+通知零事务(service.go:292-318=冻结 :692-702 逐条直译):UpdateOrgAppStatus ‘approved’ → InsertOrganization(org_,credits=0)→ InsertOrgMember(om_ role=‘owner’)→ SetUserOrg(users.org_id 回填)→ 通知「企业入驻审核通过」+组织「%s」已创建,您是管理员。(冻结 :700 逐字)。reject:无条件 UPDATE ‘rejected’,不存在也 ok:true(service.go:321-323——与 approve 严格不对称)。
5 输出与结果状态
GET 行键序 id,user_id,company,contact,reason,status,created_at,email——version int64 json:"-" 不进 JSON(model.go:177-187);reason *string NULL→null。approve {"org_id"}(冻结 :701);reject true。无 LIMIT。
6 状态流转
org_applications:pending→approved(仅此路径有守卫);pending/任意态→rejected(无守卫)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| approve 非 pending/不存在 | 404 | “not found”(英文——八态之五;approve 幂等性缺失二跑 404) |
| reject 不存在 | 200 | 恒 true(幽灵 id 静默——集成 oa_ghost2 断言) |
8 权限与数据规则
quirk:四写零事务(中途失败留半态=冻结逐条 run 同形);organizations/org_members/org_applications/users.org_id 跨 M6 共表但本域直写(model.go:3-5 自持表登记)。
F12.10 计费配置(credit-settings/model-settings 四件)
对应 PRD:§4 M12 · GET、PUT /api/admin/credit-settings、GET、PUT /api/admin/model-settings | 状态:实现 | 实现落点:admin_handlers.go:256-287、service_billing.go:3-109、repo.go:307-326 | UI:AdminView.vue(计费 tab saveBilling——只写局部零刷新三零刷新面之一)| 场景挂钩:无专项 e2e(AdminChain)
1 功能定义
system_settings KV 配置读写(五键积分+四键模型名;GET/PUT 两面共用全量十键白名单)。
2 触发条件与前置状态
四处双挂+requireDB;PUT 任意 JSON 对象——白名单外键静默忽略恒 ok(service_billing.go:80-90)。
3 输入与校验
PUT body map[string]any(bind 失败 → 400「请求体格式错误」);无键校验。
4 处理流程
GET:逐键 settingOrDefault(无行回退 defaultSettings 十键=冻结 routes2.js:26-37 DEFAULT_SETTINGS)。PUT:遍历 body → 白名单键 UpsertSetting(k, jsString(v))(ON DUPLICATE KEY upsert;值恒字符串 jsString:null→“null”/bool→“true”)。读侧零失效:genBilling/modelCfg 每请求现查 DB,UPSERT 后下一请求即生效(service_billing.go:3-5 注)。
5 输出与结果状态
credit-settings 键序 trial_credits,text_tokens_per_credit,image_generation,video_pull,video_per_second;model-settings 键序 text_model,image_model,image_model_fallback,video_model——值恒字符串;冻结键名 image_model_fallback(PRD 曾误写 image_fallback_model 勿跟)。PUT data:true。
6 状态流转
system_settings KV upsert;provider_routing 在白名单内但两 GET 均不回显(冻结 :36 同形)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| PUT 恒 | 200 | ok:true(除 bind 外永不 400) |
| 值非数字(读侧消费) | — | Go 回退默认 vs 冻结 NaN→JSON null(PRD M8 ④ 同制——getSettingNum 0 值回退 quirk) |
8 权限与数据规则
quirk:PUT /credit-settings 可写 text_model 等、PUT /model-settings 可写 trial_credits 等(同一全量十键白名单——冻结 :74-79/:84-88 同一循环同形照抄勿"修复");死端点:model-settings GET/PUT 前端零调用(PRD ②)。
F12.11 模型接入(model-endpoints 六件)
对应 PRD:§4 M12 · GET、POST /api/admin/model-endpoints、PUT、DELETE …/:id、POST …/:id/activate、POST …/:id/test | 状态:实现 | 实现落点:admin_handlers.go:289-370、service_billing.go:114-162/:182-300/:306-416、repo.go:331-386 | UI:AdminView.vue(模型接入 tab+meTest
.catch(()=>{})防御死代码注)| 场景挂钩:无专项 e2e(AdminChain;api_key 不回显断言)
1 功能定义
模型接入注册表全生命周期(列表+建+改+删+激活+三分支连通测试)。
2 触发条件与前置状态
六处双挂+requireDB;PUT/DELETE/activate/test 经 GetEndpointByID 404 前置 → 「接入不存在」(八态之六,四路同源)。
3 输入与校验
POST body purpose/name/base_url/api_key/model(purpose ∉ {text,image,video} → 400「未知模型用途」;name/base_url/model 任一空 → 400「显示名、接口地址、模型名必填」;name 截 60 rune、base_url/api_key/model TrimSpace);PUT body 五指针(nil 即未传)。bind 失败 → 400「请求体格式错误」。
4 处理流程
List:ensureEndpointSeed(表空且三 KV 全非空 → 插「文本接入(迁移)」+置 active=冻结 models.js:12-24)→ 全量列表 → maskEndpoint(api_key 非空 → 前 6 rune+******;has_key=api_key!="")→ active 三键(KV active_model_<p> → 行按 id+purpose 不查 enabled)。Create:me_ 行 enabled=1 → 该用途首个接入自动置 active(冻结 models.js:64)。Update:api_key 空串=保留旧值 SQL CASE(repo.go:374-380 CASE WHEN ? IS NULL OR ?='' THEN api_key ELSE ? END;其余 COALESCE)。Remove:删行 → 删的是 active 则 DeleteSetting(active_model_
5 输出与结果状态
GET 键序 purposes,endpoints,active(purposes 各 {name,usage} 文案=冻结 models.js:6-10 逐字);endpoints 行键序 id,purpose,name,base_url,model,enabled,created_at,api_key_masked,has_key——api_key json:"-" 不回显(二十一期②收敛,PRD ④——wire 与冻结分叉一处);active 三 *string(id‖null)。POST {"id"};PUT/DELETE true;activate {"purpose"};test {ok, reply? omitempty, note}。
6 状态流转
model_endpoints 行 + system_settings active_model_<purpose> KV;enabled 位由 PUT 任意翻转不联动 active KV(展示面 getActiveRaw 不查 enabled vs 启用面 getActive 才查——冻结两函数分工同构)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| id 不存在(四路) | 404 | 「接入不存在」 |
| purpose 非法 | 400 | 「未知模型用途」 |
| 必填空 | 400 | 「显示名、接口地址、模型名必填」 |
| test 超时/失败 | 504/502 | 「连接超时」/HTTP %d:%s(TestError 携带 Status 直出) |
8 权限与数据规则
quirk:ensureSeed 每次 GET 触发(表非空即短路);api_key 明文仍入库仅 wire 隐藏(AdminView 只消费 masked 零消费实证);测试直连用户提供的 base_url(SSRF 面与冻结同构);video 探测无鉴权头纯 GET 可达性。
F12.12 提示词中心(prompts 六件)
对应 PRD:§4 M12 · GET、POST /api/admin/prompts、POST /api/admin/prompts/upload、PUT、POST …/:id/activate、DELETE …/:id | 状态:实现 | 实现落点:admin_handlers.go:372-537、service_prompts.go:29-44/:82-175/:244-250、repo.go:393-473 | UI:AdminView.vue(prompts tab——草稿保护 dataUpdatedAt 守卫面)| 场景挂钩:无专项 e2e(集成第十五链 PromptTemplateWiring 覆盖配置即生效)
1 功能定义
八分类提示词版本管理(分类元数据+建/传/改/激活/删;POST 缺省激活、upload 恒激活)。
2 触发条件与前置状态
六处双挂(:309 upload 额外挂 up2 2mb——全域唯一三中间件端点,对齐冻结 multer fileSize);PUT/activate/DELETE 经 GetPromptByID → 404「版本不存在」(八态之七,三路共用)。
3 输入与校验
POST body category/name/script_type/content/activate *bool(activate !== false 缺省激活);upload multipart 单文件 file(FormFile 失败 → 400「缺少文件」;超 2MB → 500「File too large」multer LIMIT_FILE_SIZE 信封——状态码 500 非 400;扩展名 ∈ {.txt,.md,.markdown} 否则 400「仅支持 .txt / .md 文件」);PUT body 三 *string COALESCE;service 校验 category 八键白名单 → 400「未知的提示词分类」、content trim 空 → 400「提示词内容不能为空」。
4 处理流程
GET:ListPromptsAll(ORDER BY category, is_active DESC, created_at DESC)→ 遍历八分类(CATEGORIES=冻结 promptCenter.js:7-16 逐字)→ GetActivePrompt(key, "")(scriptType 空直落单级——三级逻辑只在 split+scriptType!="" 触发)→ count 循环。createPromptVersion 共核:name 空 → <分类名> 新版本、截 60 rune、script_type 白名单 {all,domestic,overseas} 仅 split 生效否则 ‘all’ → InsertPrompt is_active=0 → activate 则 ActivatePrompt(先全类清 0 再置 1 互斥两步,repo.go:461-467)。upload:name 缺省 regexpStripExt(originalName)(=冻结 replace(/.[^.]+$/,''))→ 恒激活。DELETE:row.IsActive → 400「生效中的版本不可删除,请先切换其他版本」→ 删。
5 输出与结果状态
GET 键序 categories,versions;categories 元素键 key,name,desc,placeholders,scriptScoped,active,count——scriptScoped 驼峰 *bool omitempty 仅 split 有键(=JS {...c} 展开只带既有键);versions 行键 id,name,category,script_type,content,is_active,created_at。POST {"id","activated"};upload {"id","name"}(fixFilename 后全名含扩展名);PUT/activate/DELETE true。无 LIMIT。
6 状态流转
prompt_templates.is_active 互斥位(每分类至多一行 1);激活即切、旧版静默下线无确认。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 分类非法 | 400 | 「未知的提示词分类」 |
| content 空 | 400 | 「提示词内容不能为空」 |
| 扩展名非法 | 400 | 「仅支持 .txt / .md 文件」 |
| 删 active 版本 | 400 | 「生效中的版本不可删除,请先切换其他版本」 |
| 无文件(upload) | 400 | 「缺少文件」 |
| 超 2MB(upload) | 500 | 「File too large」(multer LIMIT_FILE_SIZE 信封) |
| id 不存在 | 404 | 「版本不存在」 |
8 权限与数据规则
quirk:POST 缺省激活 vs instruction POST 不激活相反默认(F12.13 交叉);upload 恒激活(不同入口同效);不做 in-band ensureSeed(cmd/seed 表级哨兵补种 5 行——PRD ⑧);upload 文件全量进内存(2MB 双保险);C1 裁决注记:本域 GetActivePrompt(整行+split 三级 repo.go:413-434)↔ 生成链 activePromptContent(content 单级+content 空视同缺失 task_assembly.go:185-200)两份刻意并存不收敛(返回形态不同+domain 不可导入 package main),两处改动须同步对拍。
F12.13 指令版本(instruction-versions 六件)
对应 PRD:§4 M12 · GET、GET /api/admin/instruction-versions/active、POST、PUT、POST …/:id/activate、DELETE …/:id | 状态:实现 | 实现落点:admin_handlers.go:375-451、service_prompts.go:4-77/:216-231 | UI:AdminView.vue(指令版本 tab——十一续④拆解模板级联消费面)| 场景挂钩:无专项 e2e(集成第十五链覆盖)
1 功能定义
split 分类指令版本管理(prompt_templates category=‘split’ 薄委托——instruction_versions 死表不接线)+当前生效指令读取(三级+内置兜底)。
2 触发条件与前置状态
六处双挂+requireDB(:314 active 为静态段优先第三处);PUT/activate 404「版本不存在」(与 prompts 同源);DELETE 404 英文 “not found”(ErrVersionNotFoundEN——八态之八,与 prompts DELETE 的「版本不存在」刻意不同源照抄)。
3 输入与校验
GET active query script_type(缺省归 ‘all’=冻结 ||'all');POST body name/script_type/content(content trim 空 → 400「指令内容不能为空」);PUT body 三 *string(无 script_type 白名单直写任意值=冻结 promptCenter.js:101 原样——与 POST 白名单不对称)。
4 处理流程
versions=ListPromptsByCategory(‘split’);active=GetActivePrompt('split', scriptType) 三级(split+script_type → split+‘all’ → category 单级)→ ErrNoRows 兜底 {name:"内置默认", content: DefaultInstruction}(两形态响应:命中行=PromptRow 全键 / 未命中=两键);CreateInstruction → createPromptVersion(…, false) 不激活(与 prompts POST 相反默认);Update/Activate 与 F12.12 完全共用;DeleteInstruction 按修后语义打 prompt_templates(冻结查删 instruction_versions 死表恒 404——Go 修后:404 → IsActive 400 同文案 → 删,PRD ⑤)。
5 输出与结果状态
versions=PromptRow 数组(同 F12.12 键序);active 两形态(行全键 ‖ {name,content} 两键);POST {"id"};PUT/activate/DELETE true。
6 状态流转
prompt_templates(category=‘split’)is_active 互斥位;instruction_versions 死表(service_prompts.go:4-5 注)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| content 空(POST) | 400 | 「指令内容不能为空」 |
| PUT/activate id 不存在 | 404 | 「版本不存在」(中文——与 prompts 同源) |
| DELETE id 不存在 | 404 | “not found”(英文——八态之八刻意分叉) |
| DELETE active 版本 | 400 | 「生效中的版本不可删除,请先切换其他版本」 |
8 权限与数据规则
quirk 汇总:①POST 不激活 vs prompts POST 缺省激活相反默认;②DELETE 双 404 文案分叉(EN)+400 中文保持(PRD ⑤);③active 死端点照抄(PRD ② 前端零调用);④PUT 无 script_type 白名单与 POST 不对称;⑤CRUD 纯存储面不接生成链(生成链内置读侧照旧——改动即生效经 GetActivePrompt 查库,service_prompts.go:6 注)。
册尾端点覆盖对账(39/39)
对账 backend/cmd/server/routes.go:275-318 亲数 39 条 ↔ 本册 F12.01~F12.13。口径脚注:M12 组注释「41 物理 admin 路由 − withdrawals 2 条归 M7」(routes.go:268-270)——冻结三文件口径 index.js admin 段 17(app.* 直挂非 router)+ llmroutes.js 18 + routes2.js 4 = 39,冻结-Go 计数自洽无 m10 式错位;withdrawals 2 条(routes.go:150-151)与 submissions 2 条(:199-200)物理不在本段,归 M7/M9。
| # | 端点(routes.go:line) | 节 | # | 端点(routes.go:line) | 节 |
|---|---|---|---|---|---|
| 1 | GET /admin/summary(:275) | F12.01 | 21 | PUT /admin/model-settings(:298) | F12.10 |
| 2 | GET /admin/users(:276) | F12.02 | 22 | GET /admin/model-endpoints(:300) | F12.11 |
| 3 | PUT /admin/users/:id(:277) | F12.02 | 23 | POST /admin/model-endpoints(:301) | F12.11 |
| 4 | POST /admin/users/:id/password(:278) | F12.03 | 24 | PUT /admin/model-endpoints/:id(:302) | F12.11 |
| 5 | GET /admin/credit-grant-records(:279) | F12.04 | 25 | DELETE /admin/model-endpoints/:id(:303) | F12.11 |
| 6 | POST /admin/credit-grant-records(:280) | F12.04 | 26 | POST /admin/model-endpoints/:id/activate(:304) | F12.11 |
| 7 | GET /admin/credit-records/export(:281,静态段优先) | F12.05 | 27 | POST /admin/model-endpoints/:id/test(:305) | F12.11 |
| 8 | GET /admin/runtime/tasks(:282) | F12.01 | 28 | GET /admin/prompts(:307) | F12.12 |
| 9 | GET /admin/generation-audit/failures(:284) | F12.06 | 29 | POST /admin/prompts(:308) | F12.12 |
| 10 | POST /admin/generation-audit/videos/:id/release(:285) | F12.06 | 30 | POST /admin/prompts/upload(:309,+up2 三中间件) | F12.12 |
| 11 | POST /admin/generation-audit/tasks/:id/retry-split(:286) | F12.07 | 31 | PUT /admin/prompts/:id(:310) | F12.12 |
| 12 | GET /admin/announcements(:288) | F12.08 | 32 | POST /admin/prompts/:id/activate(:311) | F12.12 |
| 13 | POST /admin/announcements(:289) | F12.08 | 33 | DELETE /admin/prompts/:id(:312) | F12.12 |
| 14 | PUT /admin/announcements/:id(:290) | F12.08 | 34 | GET /admin/instruction-versions(:313) | F12.13 |
| 15 | GET /admin/organization-applications(:291) | F12.09 | 35 | GET /admin/instruction-versions/active(:314,静态段优先) | F12.13 |
| 16 | POST /admin/organization-applications/:id/approve(:292) | F12.09 | 36 | POST /admin/instruction-versions(:315) | F12.13 |
| 17 | POST /admin/organization-applications/:id/reject(:293) | F12.09 | 37 | PUT /admin/instruction-versions/:id(:316) | F12.13 |
| 18 | GET /admin/credit-settings(:295) | F12.10 | 38 | POST /admin/instruction-versions/:id/activate(:317) | F12.13 |
| 19 | PUT /admin/credit-settings(:296) | F12.10 | 39 | DELETE /admin/instruction-versions/:id(:318) | F12.13 |
| 20 | GET /admin/model-settings(:297,死端点②) | F12.10 |
差异纪律:PRD §4 M12 ①~⑨ 全引用不重立(含二十一期② api_key 收敛/⑥ release 认领前置+retry-split 修后/⑤ DELETE 修后语义/③ ErrNoUser 吞形/② 死端点/⑧ ensureSeed 缺位);本册新立 DIF-31-3(布尔 wire 型 enabled/active/is_active/refunded Go true/false vs 冻结 SQLite 0/1——方言级六节波及)/DIF-31-4(bind 严格性:冻结 +credits/+amount/truthy 宽收数值字符串 vs Go int64 bind 400「请求体格式错误」——F12.02/04,m08 下单 amount_cny 同型扩域)/DIF-31-5(PUT settings 非标量值字符串化 fmt.Sprint(map) vs JS "[object Object]")/DIF-31-6(upload 点文件名 .md 形态 regexpStripExt 保留 vs JS 正则清空走缺省名)入 99-附录 B。
4.5 - FSD M13 · 火花计划
← 总则 | 附录 → | 上游:PRD §4 M13 实现落点:
backend/internal/domain/spark/(model/service/repo_gorp 三件)+cmd/server/spark_handlers.go(34 行)+cmd/server/routes.go:259-262(挂载——物理嵌在 M11 段 :258 与 :263 之间)。事实基准=Go 代码;冻结版qmtv/server/index.js:739-766仅差异对拍参照。 域内要点:二十六期补登域(P0 统计残留第八例——冻结 index.js:739-766 两条端点从未入任何模块行);三阶梯 tier 表单数据源(per 系);PRD §4 M13「逐字对齐无差异」——三十期新立 DIF-30(见 m09 册/99-附录 B)。E2E-SPK-01 已实现(spark.spec.ts,清单 :15)。
M13 火花计划
2 端点全量(join+me)。零 provider 零 SSE;通知一发(join 成功);扣费面为零(唯一 Add 奖励面——反向 credits 流转)。
F13.01 火花计划(spark/join + spark/me)
对应 PRD:§4 M13 · POST /api/spark/join、GET /api/spark/me | 状态:实现 | 实现落点:spark_handlers.go:13-34、service.go:33-86、model.go:13-46、repo_gorp.go | UI:SparkView(三阶梯卡片+加入按钮)| 场景挂钩:E2E-SPK-01(已实现——重跑容错:残留轮直断投影面)
1 功能定义
编剧激励计划:一次性加入(+100 奖励)与个人阶梯投影(按 published 过审数定档)。
2 触发条件与前置状态
join:未加入前置——重复 400「您已加入火花计划」(ErrAlreadyJoined model.go:10,errRespond 400 映射,冻结 :749 同形);me:零前置(joined=false 也给完整阶梯投影,quirk)。
3 输入与校验
两端点无 body/参数(全按 token user_id)。
4 处理流程(join 四动作禁事务)
置位 users.spark_joined → credits.Add(+100, type='spark', 「加入火花计划奖励(+100)」)(service.go:44,冻结 :751 同形)→ INSERT notifications「火花计划报名成功」→ 响应 {joined: true, bonus: 100}——四动作逐序无事务(:33-53,中途失败留半态=冻结无事务同形)。
me:total=submissions 非 draft 计数(repo_gorp.go:50-56)/published/pending 双计数 → tierOf(published) 定档(0 部无命中回退首档星火,model.go:51-58)→ next_tier 环形取下一档(tierTable[(idx+1)%len],service.go:84——燎原下一档回星火,冻结 :766 同式)。
5 输出与结果状态
me 键序逐字(model.go:34-46,冻结 :762-766):joined/total/published/pending/tier/tier_perk/tier_rate/reward_earned/next_tier/tiers——reward_earned=published×per 即时计算(非累计流水);tiers=三档全量数组(min/max/per/name/perk,max *int null=无上限)。
6 状态流转
spark_joined 0→1 单向(无退出机制);阶梯随 published 增长单向升级(E2E-SUB-01 投稿过审联动)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 重复 join | 400 | 「您已加入火花计划」 |
| 0 部过审查 me | tier=星火完整投影(非空/非 404,quirk) | — |
| ErrNotConfigured | requireDB 前置后为死路径(repo_gorp.go:13/:21-26 guard,边界节素材) | — |
| bonus 与 tier per=100 | 数值偶合但不同源(service.go:44 硬编码 vs model.go:23 阶梯表——调价不同步 quirk) | — |
8 权限与数据规则
authed + amw;Add 奖励走 credits 唯一入账口(ErrNoUser 容错装配侧);零扣费零退款(全站唯一纯奖励域);wire 全蛇形零驼峰(model.go 亲证)。
册尾端点覆盖对账(2/2)
对账 backend/cmd/server/routes.go:259-262 亲数 2 条 ↔ 本册 F13.01:
| # | 端点(routes.go:line) | 节 |
|---|---|---|
| 1 | POST /spark/join(:261) | F13.01 |
| 2 | GET /spark/me(:262) | F13.01 |
差异纪律:PRD §4 M13「逐字对齐无差异」——本册零差异零新立 DIF(bonus/per 不同源与 0 部回退均系双方一致 quirk,节内登记);集成锚 SparkChain 第十六链(me 初态→join 对账→重复 400→submissions 直插阶梯联动→401)在位。
4.6 - FSD M2 · 任务与拆解
← 总则 | 附录 → | 上游:PRD §4 M2 实现落点:
backend/internal/domain/task/(model/repo/service/service_gen)+cmd/server/task_handlers.go(38 handlers 全集)+cmd/server/task_assembly.go(LLM/模板装配)+cmd/server/routes.go:324-361(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:228-366/routes2.js/routes3.js仅差异对拍参照。
M2 任务与拆解
38 端点全量(基础信封见总则 §2.2)。本域是创作主链的起点:剧本 → 拆解(规则/LLM 双引擎)→ 场景/分镜 → 资产提取与生图。计费面集中在拆解(token 制/按次 20)与图片生成(动态价);SSE 仅 split 一类事件;上传面 2 条(slots/assets,20MB)。属主判定全 SQL 谓词,命中失败一律 404 不用 403。分镜双表通路中本域只写 shots(director_shots 归 M10,零触及)。
F2.01 任务列表
对应 PRD:§4 M2 · GET /api/tasks | 状态:实现 | 实现落点:task_handlers.go:51-56、task/repo.go:120-125 | UI:
/dashboard(DashboardView 任务列表)| 场景挂钩:E2E-DASH-01(A1 回归锁:wire=驼峰 shotCount)/E2E-NAV-01
1 功能定义
当前用户任务列表倒序读,每行附镜数(shotCount 驼峰 wire——二十六期 A1 回归根因,e2e 回归锁钉死)。
2 触发条件与前置状态
- 进入拆解台/任务列表刷新 →
GET /api/tasks(Bearer);降级模式 503。
3 输入与校验
无参数。身份取自 JWT claims。
4 处理流程
SELECT t.*, (子查询 COUNT shots) AS shot_count … WHERE t.user_id=? ORDER BY t.created_at DESC → TaskWithCount 数组。宽扫描 t.* 依赖 0002 DDL 收紧(W8b 守卫 integration_test.go:461-484)。
5 输出与结果状态
200 {"ok":true,"data":[TaskWithCount…]}。行 wire:id/user_id/title/status/error/script_type/face_style/min_duration/max_duration/notes/extract_status/assets_data/split_status/style_id/extract_scope/extract_notes/created_at/updated_at(蛇形)+ shotCount(驼峰,model.go:60-63);script_text 指针 nil→JSON null。空列表 data:null(gorp nil slice——冻结恒 [],DIF-29-1)。
6 状态流转
无(纯读)。status 取值域见 F2.03 §6。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无任务 | 200 data:null(非 [],DIF-29-1) | — |
| DB 错误 | 500 | internal server error |
8 权限与数据规则
- 权限:登录态;
WHERE user_id=?谓词内联。 - 数据规则:倒序
created_at DESC(无次级排序键,同秒行序可能抖动——冻结同构)。
F2.02 剧本拆解·旧版同步
对应 PRD:§4 M2 · POST /api/tasks | 状态:实现 | 实现落点:task_handlers.go:62-82、task/service.go:148-167/301-327 | UI:拆解台旧版提交入口| 场景挂钩:无(V2 为主链,本端点为冻结 legacy 保真)
1 功能定义
同步规则拆解:扣固定 20 积分 → 确定性 preprocess 切分场次/分镜/资产 → 立即返回统计。无 LLM、无异步。
2 触发条件与前置状态
- 拆解台提交(legacy 形态)→
POST /api/tasks(Bearer)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| title | JSON body | 否 | 缺省「未命名」,截 60 rune(冻结 INSERT 用「未命名剧本」而台账 desc 用「未命名」——Go 把 desc 缺省同化进 title,DIF-29-13) |
| script_text | JSON body | 是 | TrimSpace=="" → 400 剧本内容不能为空(冻结 index.js:236 同文案) |
4 处理流程
flowchart TD
A["POST /tasks"] --> B["剧本非空校验"]
B --> C["TryDeduct 20<br/>type=preprocess<br/>「剧本拆解:<title>(-20)」"]
C -->|不足| D["402 积分不足"]
C --> E["INSERT tasks status=complete"]
E --> F["fillWithMock:preprocess 确定性切分"]
F --> G["InsertAssets + InsertShots + 每镜 slot1 故事板"]
G --> H["200 {id,sceneCount,shotCount}"]扣费在插任务前,Insert 失败不退(service.go:159-163,冻结同序)。三次 Insert 均 _ = 吞错(service.go:314/318/324)。INSERT 显式写 18 列零值(script_type:''/min_duration:0/split_status:'' 等覆盖 DDL 默认——冻结走 SQLite DEFAULT,DIF-29-5)。
5 输出与结果状态
200 {"ok":true,"data":{"id","sceneCount","shotCount"}}(sceneCount/shotCount 驼峰,task_handlers.go:77-81;冻结 index.js:252 同)。
6 状态流转
tasks.status:插入即 'complete'(无 processing 中间态);split_status:INSERT 显式写 ''(18 列零值,DIF-29-5——非 DDL 默认 'done');本端点不消费该列。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 剧本空白 | 400 | 剧本内容不能为空 |
| 余额 <20 | 402 | 积分不足(零台账) |
| Mock 拆解 | 确定性:场次按「第X场」标记切块(无标记按空行 >10 rune);每场 3 | — |
8 权限与数据规则
- 权限:登录态;创建即本人(user_id 取 claims)。
- 数据规则:写
tasks/shots/assets/shot_images/credit_records五表。
F2.03 剧本拆解·V2 双引擎
对应 PRD:§4 M2 · POST /api/tasks/v2 | 状态:实现 | 实现落点:task_handlers.go:140-160、task/service.go:241-299/330-366 | UI:拆解台主提交(DashboardView)| 场景挂钩:E2E-DASH-01(Mock 同步分支:toast「拆解完成」+镜数>0)
1 功能定义
token 制计费的剧本拆分:LLM 可用走异步拆解(SSE split 事件+站内通知,失败回退规则拆解);LLM 不可用走规则拆解同步返回。计费 ceil(tokens/1000) 上限 1。
2 触发条件与前置状态
- 拆解台提交(主链)→
POST /api/tasks/v2(Bearer)。 - LLM 可用判定
llm.Config.Available()(三元全非空,llm.go:24);配置三级链 active_model_text→model_endpoints→KV llm_*(task_assembly.go:49-59)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| title | JSON body | 否 | 缺省「未命名」截 60 |
| script_text | JSON body | 是 | 空 → 400 剧本内容不能为空。唯一校验 |
| script_type / face_style | JSON body | 否 | orDefault 缺省 domestic/asian_chinese——无冻结二值化归一(任意值透传,DIF-29-14) |
| min_duration / max_duration | JSON body | 否 | maxInt(入参,1)/maxInt(入参,3) |
| notes / style_id | JSON body | 否 | style_id 缺省 realfilm |
4 处理流程
flowchart TD
A["POST /tasks/v2"] --> B["剧本非空校验"]
B --> C["token 估算:max(1000, ceil(rune数×1.6))<br/>cost=ceil(est/tokensPerCredit) <1 归 1"]
C --> D["TryDeduct type=preprocess task_id=id<br/>「剧本拆分(约N tokens / 模型名,-cost)」"]
D --> E["INSERT tasks status=processing<br/>split_status= llm可用?processing:done"]
E --> F{"llm.Available()?"}
F -->|否| G["fillWithMock + status=complete<br/>同步返回 sceneCount/shotCount"]
F -->|是| H["go runLLMSplit(异步)<br/>响应 {id,async:true,engine,cost,tokens_estimated}"]
H --> I["SplitTemplate 三级 → LLM Chat 240s → ParseSplit"]
I -->|成功| J["三删重建 shots/assets + slot1<br/>SetSplitDone + 通知「剧本拆解完成」+ SSE split done"]
I -->|失败| K["fallbackMock:三删+fillWithMock+SetSplitDone+complete<br/>通知「AI 拆解失败,已回退规则拆解」+ SSE split fallback"]
K -->|回退也失败| L["status=failed + split_status=failed"]runLLMSplit 后台 goroutine defer recover() 兜底(service.go:332);重建三删序 ClearShotImages→DeleteShotsByTask→DeleteRootAssetsByTask(service.go:411-419)。
5 输出与结果状态
V2Result 键序 id, sceneCount(omitempty), shotCount(omitempty), async, engine, cost, tokens_estimated(service.go:243-251):LLM 分支无 sceneCount/shotCount 键(冻结同);Mock 分支 async:false + 两计数。
6 状态流转
tasks.status:processing(仅本端点插入)→complete(Mock/LLM 完成/回退成功/restart/启动恢复)|failed(LLM 双重失败、mark-failed)。tasks.split_status:DDL 默认'done'|'processing'(LLM 可用)→'done'(完成/回退)|'failed'(双重失败)。代码无'idle'/'fallback'写入点(PRD :142 状态机表含此二态——PRD 该行已加注「冻结与 Go 均无写入点」,用户裁决〔三十期〕判定非系统需要不实现:前端零消费〔DashboardView 仅认 processing/failed〕+fallback 系同步内联回退无可观察窗口,勿照抄亦勿实现)。- 服务重启悬挂行:
RecoverProcessingSplits(service.go:444-463)扫split_status='processing'→ fillWithMock 直插不删基座 → 恢复完成,通知「拆解已回退完成」,无 SSE 帧。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 剧本空白 | 400 | 剧本内容不能为空 |
| 余额不足 | 402 | 积分不足 |
| LLM 失败 | 不回 HTTP(异步已 200);回退规则拆解+通知+SSE fallback | — |
| 回退再失败 | status/split_status 双 failed(已扣费不退——冻结同) | — |
| token 估算 | rune 数×1.6(冻结 UTF-16 .length,BMP 外计数不同,DIF-29-15);计费 ceil(冻结 round——credits.TextCost 正确 round 实现存续未用 billing.go:118-131,DIF-29-6) | — |
| 模型名缺配置 | 台账 desc 出现「…tokens / ,-N)」空串形态(冻结缺行回「星芒文本引擎」,DIF-29-7) | — |
8 权限与数据规则
- 权限:登录态;写
tasks/shots/assets/shot_images/credit_records/notifications。 - 数据规则:TryDeduct 带 task_id=id(冻结 routes2.js:158 同);通知两文案冻结逐字(service.go:352-353/359-361)。
F2.04 任务详情读取(:id / full / full2)
对应 PRD:§4 M2 · GET /api/tasks/:id、GET /api/tasks/:id/full、GET /api/tasks/:id/full2 | 状态:实现 | 实现落点:task_handlers.go:84-97/275-297、service_gen.go:300-427 | UI:VideoView/DashboardView 任务详情| 场景挂钩:E2E-NAV-01(读面巡检)
1 功能定义
任务详情三形态:基础(shots+原始 assets)、full(assets 带图 URL+assets_data 缓存)、full2(variants 衍生体+风格名/缩略图+组间尾帧衔接)。
2 触发条件与前置状态
- 详情页/分镜工作台加载 → 三端点(Bearer);任务不存在或非本人 → 404。
3 输入与校验
路径 :id。无 body。
4 处理流程
- :id:
ByID(WHERE id=? AND user_id=?)→ShotsWithSlots(shots ORDER index_no + 槽位按 shot 分组 ORDER slot_no + refAssetCount=COUNT assets)→ 原始 assets(无 image_url,task_handlers.go:95——冻结 index.js:261 同 quirk)。 - full:ByID → ShotsWithSlots → AssetsByTask(LEFT JOIN images 取 image_url)→ assets_data=缓存优先(坏 JSON 宽容 nil)否则从根资产现拼(5 键 item:id/name/description/image_id/image_url——冻结塞原始全列行,DIF-29-16)。
- full2:再加 VariantsByTask(parent_id<>’’)合并进缓存/fallback;
tail_by_group组间尾帧 map(组内最后有图镜 image_id,首镜且 groupNo>1 配prev_tail_image_id,service_gen.go:355-372);style_name=StyleName(19 风格目录缺省「真人电影」)、style_thumb幂等落盘style_<id>.svg(不在目录→null)。
5 输出与结果状态
- :id →
{"task","shots":[ShotView],"assets":[原始行]};shots 空为[](make 初始化)、slots/assets 空null(DIF-29-1)。 - full →
{"task","shots","assets","assets_data"}。 - full2 →
{"task":{…Task,"style_name","style_thumb"},"shots","assets","variants","assets_data","tail_by_group"}——style_name 嵌套在 task 内(前端 curTask.style_name 消费)。
6 状态流转
无(纯读)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非本人/不存在 | 404 | not found |
| assets_data 缓存坏 JSON | 静默忽略走 fallback 拼装 | — |
| style_id 不在目录 | style_name=「真人电影」、style_thumb=null | — |
8 权限与数据规则
- 权限:登录态 + 任务属主。
- 数据规则:ShotView 含
slots/refAssetCount(驼峰)/prev_tail_image_id(full2 组首镜);variants 键映射 parent_id→item 数组。
F2.05 任务生命周期管理(title/delete/duplicate/mark-failed/restart)
对应 PRD:§4 M2 · PUT /api/tasks/:id/title、DELETE /api/tasks/:id、POST /api/tasks/:id/duplicate、POST /api/tasks/:id/mark-failed、POST /api/tasks/:id/restart | 状态:实现 | 实现落点:task_handlers.go:99-138、task/service.go:168-230/583-587、task/repo.go:146-175/234-248 | UI:任务卡操作菜单| 场景挂钩:无独立 e2e
1 功能定义
任务五个管理动作:改名、删除(三删序+孤儿保留)、复制(含分镜/资产快照)、标记失败、重启清图。
2 触发条件与前置状态
- 任务卡操作 → 对应端点(Bearer);title/delete/duplicate 404 面,mark-failed 无存在性检查。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| PUT /tasks/:id/title | {"title":string} | 截 60 rune;无 404 面(0 行仍 200,冻结同) |
| DELETE /tasks/:id | — | ByID 谓词内联;0 行 → 404 not found |
| POST /tasks/:id/duplicate | — | ByID 404 面 |
| POST /tasks/:id/mark-failed | — | 无校验(0 行也 200,冻结 routes2.js:250 同) |
| POST /tasks/:id/restart | — | ByID 404 面 |
4 处理流程
- title:
UPDATE tasks SET title=?, updated_at=NOW(3) WHERE id=? AND user_id=?。 - delete:三删序 shots→assets→tasks(子查询属主谓词);不删 shot_images/image_histories/images/videos(孤儿保留,repo.go:151 注释冻结 §1A)。
- duplicate:新任务 title=
原题+"-副本"截 60(DIF-29-10),status/script_text 沿源(nil 透传);逐镜复制(image_id/video_id 清空);仅复制源镜存在 slot_no=1 的槽位(service.go:200-204,冻结每镜无条件插——DIF-29-10);assets 全复制但 parent_id 不带(衍生变根资产,冻结 index.js:290-292 同 quirk)。 - mark-failed:
UPDATE tasks SET status='failed';不触 split_status。 - restart:
RestartClears四步——shots.image_id=’’+status=‘idle’、shot_images.image_id=’’、tasks status=‘complete’+extract_status=’’+assets_data=’’、assets.image_id=’’;保留 shots/assets/槽位行(NULL 语义翻译 ‘’,model.go:4-5)。
5 输出与结果状态
五者均 {"ok":true,"data":true};duplicate 另返 {"id":"<新任务>"}。
6 状态流转
- delete:行删除(级联仅 shots/assets 两层)。
- mark-failed:
complete|processing → failed。 - restart:
任意 → complete;shotsdone → idle;extract_status →''。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| title 改他人任务 | 0 行 200 true(无存在性检查) | — |
| delete/mark-failed 非本人 | delete 404;mark-failed 0 行 200 | not found |
| duplicate 长 title | 截 60(冻结不截,DIF-29-10) | — |
8 权限与数据规则
- 权限:登录态;delete/duplicate/restart 谓词内联属主。
- 数据规则:restart 是「清成果不删结构」语义——与 delete(删结构留孤儿)互补。
F2.06 智能资产提取(extract / extract2 / extract-settings)
对应 PRD:§4 M2 · POST /api/tasks/:id/assets/extract、POST /api/tasks/:id/assets/extract2、PUT /api/tasks/:id/extract-settings | 状态:实现 | 实现落点:task_handlers.go:174-208、service_gen.go:34-159 | UI:拆解台资产区「智能提取」| 场景挂钩:无 e2e(人工清单 T2 族)
1 功能定义
对拆解产出的角色/场景/道具资产批量生图:v1 按资产类型模板出图并写 assets_data 缓存;v2 支持 scope 范围+自定义要求+风格前缀;extract-settings 持久化提取范围与要求。
2 触发条件与前置状态
- 资产区按钮 → extract/extract2(Bearer);设置面板保存 → extract-settings。前置:任务已有资产行。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| extract (v1) | 无 body | 资产为空 → 400 该任务没有可提取的资产 |
| extract2 (v2) | 无 body | scope 内(根资产 ∩ extract_scope)为空 → 400 提取范围内没有资产(可在自定义提取设置中调整范围) |
| extract-settings | {"scope":[...],"notes":string|null} | scope 白名单过滤 character/scene/prop 后 join(空数组→’’,冻结同);notes 非 null 才覆盖 |
4 处理流程
flowchart TD
A["POST /assets/extract(2)"] --> B["ByID 属主 404 面"]
B --> C{"有资产?"}
C -->|否| D["400"]
C --> E["先 UpdateExtractResult done,''"]
E --> F["逐资产:AssetPrompt 模板+风格前缀(v2)"]
F --> G["images.Generate(Mock SVG/真实通道)"]
G -->|单张失败| H["退单价 + 502 文案"]
G -->|成功| I["InsertImage kind=asset + UpdateAssetImage"]
I --> J["UpdateExtractResult done, assets_data JSON"]- 计费:unit=ImageCost(settings
image_generation默认 10);v1 cost=unit×资产数(含衍生体),desc智能资产提取(N 个资产 × 10,-<cost>);v2 cost 同式,desc智能资产提取 ×N(风格:<风格名>,-<cost>);type=image。 - v2 prompt=「视觉风格:<风格名>。」+AssetPrompt+(ExtractNotes?
\n[生成要求] <notes>)。 - v1 失败 502 文案金额用总额(冻结 v1 用单价——退款金额本身正确,DIF-29-2);v2 失败退全额(冻结 v2 传总额笔误的保真对齐,service_gen.go:121-123 注释)。
5 输出与结果状态
{"extract_status":"done","assets_data":{"characters":[],"scenes":[],"props":[]}};item 键 id/name/description/image_id/image_url/prompt——v2 冻结显式带 variants:[],Go omitempty 键缺失(DIF-29-3)。
6 状态流转
tasks.extract_status:'' → 'done'(成功路径先行置位,失败不回滚——下次重试覆盖);assets_data 缓存同步覆盖。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无资产 | 400 | 该任务没有可提取的资产 |
| scope 内为空 | 400 | 提取范围内没有资产(可在自定义提取设置中调整范围) |
| 余额不足 | 402 | 积分不足 |
| 单张生成失败 | 退单价 continue + 最终 502 | 图片生成失败:<cause>(<cost> 积分已退回) |
8 权限与数据规则
- 权限:登录态+任务属主;写
images/assets/tasks/credit_records。 - 数据规则:v2 targets=根资产(parent_id=’’)∩scope;衍生体在 v1 中也计费(含衍生体)。
F2.07 AI 重新拆解(resplit)
对应 PRD:§4 M2 · POST /api/tasks/:id/resplit | 状态:实现 | 实现落点:task_handlers.go:514-533、service.go:488-573 | UI:分镜区「AI 重新拆解」| 场景挂钩:无 e2e
1 功能定义
按自然语言指令对已有分镜做密度调整重切:更多/细致/丰富→每场 5 镜、精简/减少/合并→每场 2 镜、缺省 3~4;固定扣 20 积分。
2 触发条件与前置状态
- 分镜区操作 →
POST /api/tasks/:id/resplit(Bearer);前置:任务存在(不要求 complete)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| instruction | JSON body | 否 | trim 后密度匹配;无 400 面 |
4 处理流程
ByID → 扣 20(desc AI 重新拆解(<instruction截16>)(-20),空指令 AI 重新拆解(-20),type=preprocess)→ 三删重建(同 F2.03)→ preprocess 密度调参重切 → 重插 shots(每组首镜 independent 余 anchor_prev)+slot1 → SetSplitInstruction(存 trim 值)。
5 输出与结果状态
{"instruction","sceneCount","shotCount"}——instruction 回显的是请求原始值非 trim 值(task_handlers.go:524-532;冻结回显 trim 后值,DIF-29-8)。
6 状态流转
shots 全量重建(id 全新);split_instruction 列更新;status/split_status 不变。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非本人 | 404 | not found |
| 余额 <20 | 402 | 积分不足 |
| 指令无密度词 | 缺省密度 3~4 镜/场 | — |
8 权限与数据规则
- 权限:登录态+属主;写
shots/shot_images/tasks/credit_records。
F2.08 分镜图批量与续生成(continue / auto-generate / cancel / storyboard-batch)
对应 PRD:§4 M2 · POST /api/tasks/:id/continue、POST /api/tasks/:id/scenes/auto-generate、POST /api/tasks/:id/scenes/auto-generate/cancel、POST /api/tasks/:id/storyboard-batch | 状态:实现 | 实现落点:task_handlers.go:299-334/346-357/535-553、service.go:590-703、service_gen.go:634-695 | UI:分镜区批量操作| 场景挂钩:无 e2e(人工清单 T2/T3 族)
1 功能定义
四个批量/续生成入口:continue 补缺图镜、auto-generate 带连续性提示词串联生成、cancel 纯 stub 停止提示、storyboard-batch 按模式(全部/补缺/选中)批量生图。
2 触发条件与前置状态
- 分镜区批量按钮 → 对应端点(Bearer)。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| continue | 无 body | 404 面;无缺图镜时 cost=0 不扣(len>0 才 TryDeduct) |
| auto-generate | 无 body | 404 面;len=0 也 TryDeduct(0) 落 0 元台账行(冻结 util.js:86-91 同 quirk) |
| auto-generate/cancel | 无 body | 纯 stub 无校验 |
| storyboard-batch | {"mode":"missing"|"selected"|"all","shot_ids":[]} | mode 空→missing;空列表 → 400 没有缺失的图片(missing)/未选择任何分镜(selected);all+ids 取交集 |
4 处理流程
- continue:pending=缺图镜(image_id=’’/NULL)→ cost=unit×len(desc
继续生成未完成分镜 ×N(-<cost>))→ 逐镜 Generate(“16:9”,“分镜<index_no>”) → settleShotImage(InsertImage kind=storyboard+SetShotImage done+slot1 回填+InsertHistory)。 - auto-generate:list=pending 空则全量镜;prompt 追加连续性段——非首镜
\n[连续性] 与上一镜头(<prevURL||前帧>)保持人物/光影/机位连续。、首镜\n[起始镜头] 由智能体判断开场构图。;desc故事板全自动串联生成 ×N(-<cost>)。 - cancel:
{"cancelled":true,"note":"已停止后续镜头的排队(已产生的费用不退)"},不写任何表。 - storyboard-batch:desc
批量生图(全部|仅补缺失|选中 ×N,-<cost>);连续性措辞与 auto-generate 不同源(「与上一镜(<prevURL||前帧>)保持一致。」/首镜「智能体判断开场构图」);失败退单价(冻结传总额系笔误,Go 统一单价——PRD v1.3 已登记,service_gen.go:632-633)。
settle 失败(DB 错)时该张已生成不退费、仅 failed++(service.go:616-619/689-694,冻结同)。
5 输出与结果状态
continue/auto-generate/storyboard-batch → {"generated":N,"failed":N};cancel → 上述 stub 体。
6 状态流转
成功镜:shots.status idle → done(SetShotImage);失败镜 status 不动(仅计数)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非本人 | 404 | not found |
| 余额不足 | 402 | 积分不足 |
| selected 空数组 | 400 | 未选择任何分镜 |
| missing 无缺图 | 400 | 没有缺失的图片 |
| 单张失败 | 退单价 continue | 502 图片生成失败:… |
8 权限与数据规则
- 权限:登录态+属主;写
images/shots/shot_images/image_histories/credit_records。 - 数据规则:三批量端点计价均为动态 unit(settings 可调)——与 F2.09 的静态价不同源。
F2.09 单镜故事板生成(images/storyboard/generate)
对应 PRD:§4 M2 · POST /api/images/storyboard/generate(v1.3 W10 归组 M2——物理在冻结 index.js 生成段)| 状态:实现 | 实现落点:task_handlers.go:420-441、service_gen.go:581-623 | UI:分镜卡「生成」/「2K 预览」| 场景挂钩:间接 E2E-NAV-01
1 功能定义
单镜故事板图/2K 预览生成:静态价(图 10/预览 15,无 settings 键),可选关联 shot 回填。
2 触发条件与前置状态
- 分镜卡生成按钮 →
POST /api/images/storyboard/generate(Bearer)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| shot_id | JSON body | 否 | 非空→ShotByID+属主校验(err→404 shot not found);空则跳过校验也可用(冻结 index.js:345 同) |
| prompt | JSON body | 否 | 缺省取 shot.ImagePrompt |
| ratio | JSON body | 否 | — |
| preview | JSON body | 否 | true→2K 形态 |
4 处理流程
计费 cost=10(COSTS.image)/15(COSTS.image_2k 恒定无 settings 键,billing.go:72-73);kind=‘storyboard’/‘preview_2k’;desc 分镜图生成(-10)/2K预览(-15)(冻结「2K预览生成(-15)」缺「生成」二字,DIF-29-9);task_id=shot 有则其 task 否则 ‘’。Generate → 失败退同额+502;成功 InsertImage(prompt=请求原值非解析值)→ sh != nil && !preview 才回填 shots/slot1/histories(linkShotImage 不重复插 images——W3 冒烟双插主键冲突修,service_gen.go:615-621 注释)。
5 输出与结果状态
{"id","url"}。
6 状态流转
关联 shot 非预览时:shots.status → done、slot1 回填、histories +1。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| shot_id 给了但不存在/非本人 | 404 | shot not found |
| 余额不足 | 402 | 积分不足 |
| 生成失败 | 退款+502 | 分镜图生成(-10)失败自动退回族文案 |
8 权限与数据规则
- 权限:登录态;shot 关联时校验属主。
- 数据规则:静态价与 F2.08 动态价刻意不同源(billing.go:72-73 注释);Mock label=desc 全串(冻结 label「分镜图」/「2K预览」——影响 Mock SVG 文件名,DIF-29-9)。
F2.10 场景分组管理(scenes2 / rename / delete)
对应 PRD:§4 M2 · GET /api/tasks/:id/scenes2、PUT /api/tasks/:id/scenes/:groupNo、DELETE /api/tasks/:id/scenes/:groupNo | 状态:实现 | 实现落点:task_handlers.go:472-512、repo.go:356-371 | UI:分镜区场景分组栏| 场景挂钩:无 e2e
1 功能定义
场景分组聚合读与改名/删除:scenes2 按 group_no 聚合统计,rename/delete 按 group_no 批量作用。
2 触发条件与前置状态
- 分镜区场景栏 → 三端点(Bearer)。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| GET scenes2 | — | ByID 404 面 |
| PUT scenes/:groupNo | {"scene_title":string} 截 60 | groupNo 非数字→归 0(task_handlers.go:493-494;冻结 NaN 绑定 TypeError 500——Go 200 零行,DIF-29-11) |
| DELETE scenes/:groupNo | — | 同上;0 行也 200 |
4 处理流程
- scenes2:
SELECT group_no, MIN(scene_title), COUNT(*) shot_count, MIN(index_no) first_index … GROUP BY group_no ORDER BY group_no(MIN(scene_title) 字典序最小而非首镜标题——冻结同 SQL quirk 保真)。 - rename:
UPDATE shots SET scene_title=? WHERE task_id=? AND group_no=?。 - delete:
DELETE FROM shots WHERE task_id=? AND group_no=?。
5 输出与结果状态
scenes2 → [{"group_no","scene_title","shot_count","first_index"}](全蛇形,空→null DIF-29-1);rename/delete → data true。
6 状态流转
delete 组删除后 shots 行消失(槽位/histories 孤儿保留)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| groupNo=abc | 折 0 匹配零行 200(DIF-29-11) | — |
| 组不存在 | rename/delete 0 行 200 | — |
8 权限与数据规则
- 权限:登录态+属主(task_id 谓词)。
F2.11 分镜编辑与槽位(shots / slots / shot-images / histories / shots2)
对应 PRD:§4 M2 · PUT /api/shots/:id、DELETE /api/shots/:id、POST /api/shots/:id/slots、DELETE /api/shot-images/:slotId、GET /api/shots/:id/histories、PUT /api/shots2/:id/params | 状态:实现 | 实现落点:task_handlers.go:359-470、service.go:492-501/570-578、service_gen.go:504-559、repo.go:322-354/408-437/465-470 | UI:分镜卡编辑器| 场景挂钩:无独立 e2e
1 功能定义
单镜六动作:三提示词编辑、删除、参考槽位添加(上传或生图)、槽位删除(slot1 保护)、历史读、分辨率参数。
2 触发条件与前置状态
- 分镜卡操作 → 六端点(Bearer);slots 上传限额 20MB(up20)。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| PUT /shots/:id | image_prompt/video_prompt/transition_mode 均可空指针 | null=跳过该键(COALESCE 部分更新契约 §6);属主两步查(ShotByID+task 属主)失败 404 shot not found |
| DELETE /shots/:id | — | 404 面;不删槽位/histories/images |
| POST /shots/:id/slots | multipart file + form prompt/kind/ratio/label | 无文件≠错误——无文件走生图分支扣 10 |
| DELETE /shot-images/:slotId | — | slot_no=1 → 400 故事板槽位不可删除;其余 404 面 |
| GET /shots/:id/histories | — | 404 面 |
| PUT /shots2/:id/params | {"resolution":string} | 非 720p/1080p 静默忽略仍 200(冻结 routes3.js:341-343 同) |
4 处理流程
- slots 双路:有文件→images.Upload 零费;无文件→静态价 10(desc
镜头图片生成(-10),task_id=’’)→Generate→失败退+502。成功:slotNo=MAX(slot_no)+1、InsertImage(kind=orDefault(kind,“anchor”))、InsertShotImages(label=orDefault(label,“参考图”));生成路另 SetShotImage(done)+InsertHistory( _ =吞错)。 - shotOwned(service.go:492-501):ShotByID err→ErrShotNotFound;task 属主 err→同——两步均 404
shot not found。
5 输出与结果状态
- slots →
{"slot_no":int,"image_id","url"};histories →[{"id","shot_id","image_id","prompt","created_at","file_path"}](空→null DIF-29-1);其余 data true。
6 状态流转
- 生图路 slots/SetShotImage:shots.status → done。
- params:resolution 列更新(白名单内才生效)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| shot 非本人 | 404 | shot not found(六端点同哨兵) |
| 删 slot1 | 400 | 故事板槽位不可删除 |
| slots 无文件且余额不足 | 402 | 积分不足 |
| slots 生图失败 | 退单价 | 502 |
| resolution=480p | 静默忽略 200 | — |
8 权限与数据规则
- 权限:登录态+shot→task 属主链。
- 数据规则:本域写 images 表为跨域共表直插(10 列形态,repo.go:551-556,主归属 generation——总则 §1 术语);histories 行 id 前缀
si_(冻结ih_;ids.PrefixImageHistory=“ih_” 仅 director 域消费(repo.go:319 InsertImageHistory),前缀双轨并存——DIF-29-4,99-附录:57)。
F2.12 资产图编辑与衍生(assets upload / regenerate / derive)
对应 PRD:§4 M2 · POST /api/assets/:id/image/upload、POST /api/assets/:id/regenerate、POST /api/assets/:id/derive | 状态:实现 | 实现落点:task_handlers.go:213-272、service_gen.go:162-276 | UI:资产卡操作| 场景挂钩:无 e2e
1 功能定义
资产图三动作:手动上传替换(同步 assets_data 缓存)、局部修改生图、衍生变体(新建子资产行)。
2 触发条件与前置状态
- 资产卡操作 → 三端点(Bearer);upload 限额 20MB。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| upload | multipart file | 无文件 → 400 缺少文件(冻结 routes2.js:217 同文案);资产 404 not found |
| regenerate | {"instructions":string} | trim 空 → 400 请填写局部修改说明 |
| derive | {"variant_desc":string} | trim 空 → 400 请描述变体(如:换一套服装 / 中老年形象) |
4 处理流程
- upload:AssetByID(JOIN tasks 属主)→ Upload(FixFilename)→ InsertImage(kind=‘asset’) → UpdateAssetImage → 同步 assets_data 缓存(读改写 JSON 三组逐项改 image_id/image_url,坏 JSON 静默跳过)。
- regenerate:扣 unit(desc
资产图局部编辑:<名>(-<unit>))→ prompt=AssetPrompt+\n[局部修改] <ins>(其余部分保持原图不变)→ Generate → 失败退+502 → InsertImage(kind=‘asset_edit’) → UpdateAssetImage(不写缓存,冻结同)。 - derive:扣 unit(desc
角色衍生:<名>·<variant截12>(-<unit>))→ 先插子资产行(name=<名>_衍生<N>,parent_id=源)→ prompt=AssetPrompt+\n[衍生变体] <v>。保持角色五官与身份一致,仅按描述改变形态。视觉风格:<风格名>。→ Generate → 失败退+502(子资产行残留无回滚——冻结同序 routes3.js:170-172)→ InsertImage(kind=‘asset_variant’) → UpdateAssetImage(子)。
5 输出与结果状态
upload/regenerate → {"image_id","url"};derive → {"asset_id","image_id","url","name"}。
6 状态流转
- derive:assets 树新增子节点(parent_id 链)。
- upload:assets.image_id 替换+缓存同步;regenerate 仅替换 image_id。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 缺文件 | 400 | 缺少文件 |
| 说明/变体描述空 | 400 | 上述两句 |
| 余额不足 | 402 | 积分不足 |
| 生成失败 | 退单价 | 502 图片生成失败:… |
| derive 生成失败 | 退款但子资产行残留 | — |
8 权限与数据规则
- 权限:登录态+资产→任务属主链(AssetByID JOIN tasks)。
- 数据规则:FixFilename 处理中文文件名(总则坑表);缓存同步只覆盖命中的 name 项。
F2.13 任务台账与资产库(credit-records / library-apply / asset-library)
对应 PRD:§4 M2 · GET /api/tasks/:id/credit-records、POST /api/tasks/:id/library-apply/:libId、GET/POST /api/asset-library、POST /api/asset-library/:id/share、DELETE /api/asset-library/:id | 状态:实现 | 实现落点:task_handlers.go:555-631、service_gen.go:705-764、repo.go:569-605 | UI:任务详情台账页/素材库页| 场景挂钩:E2E-CRD-01(台账键形同源 credits.Record)
1 功能定义
任务维度台账对账读(LIMIT 200+汇总)、资产库条目应用到任务、素材库个人/组织共享 CRUD。
2 触发条件与前置状态
- 任务详情台账 tab → credit-records;素材库页 → asset-library 四动作;「应用到任务」→ library-apply。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| GET credit-records | — | 任务 404 not found |
| POST library-apply/:libId | — | 任务 → 404 task not found;库条目 → 404 asset not found(双哨兵分叉) |
| GET /asset-library | — | orgIDOf 查失败按无组织 |
| POST /asset-library | {"name","type","description","image_id","asset_id"} | trim(name)=="" → 400 名称必填;asset_id 查不到静默忽略;type 缺省 character |
| POST …/:id/share | — | 非本人 404 not found;无组织 → 400 请先加入企业组织 |
| DELETE …/:id | — | 不存在也 200(service_gen.go:743-746 注释) |
4 处理流程
- credit-records:credits.RecordsByTask(LIMIT 200 ORDER created_at DESC,id DESC)+ 汇总(负额和+by_type 分组,service.go:79-93)。
- library-apply:双 404 面 → InsertAssets(desc=lib.Description+
(来自资产库),image_id 沿库)。 - asset-library 列表:
(l.user_id=? OR (org_shared=1 AND l.user_id IN (SELECT id FROM users WHERE org_id=?)))ORDER created_at DESC LIMIT 200 JOIN users 取 owner_email(共享子查询走 users.org_id 单组织模型——冻结 org_members 双向 JOIN,语义等价登记 repo.go:574)。 - share:翻转 org_shared → 返回
{"org_shared":0|1}(整数)。
5 输出与结果状态
- credit-records →
{"list":[Record…],"summary":{"total_spent","by_type"},"task_title"};Record wireid,user_id,task_id,amount,balance_after,type,description,created_at(task_id NULL 行 wire “")。 - asset-library →
[LibraryView…](+owner_email,空→null DIF-29-1);add →{"id"};apply →{"asset_id"}。
6 状态流转
- library-apply:assets 表新增行(任务下)。
- share:org_shared 0⇄1 翻转。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务不存在 | credit-records 404 not found;apply 404 task not found | — |
| 库条目不存在 | apply 404 asset not found | — |
| 名称空 | 400 | 名称必填 |
| 无组织 share | 400 | 请先加入企业组织 |
| 删不存在条目 | 200 true | — |
8 权限与数据规则
- 权限:登录态;asset-library 写面全部
user_id=?谓词(share 的组织可见性靠 org_shared 列+列表子查询)。 - 数据规则:台账读面与 M1 F1.06 全局流水同源 credits 域;LIMIT 200。
M2 端点覆盖对账
| # | 端点 | 功能节 |
|---|---|---|
| 1 | GET /api/tasks | F2.01 |
| 2 | POST /api/tasks | F2.02 |
| 3 | POST /api/tasks/v2 | F2.03 |
| 4 | GET /api/tasks/:id | F2.04 |
| 5 | PUT /api/tasks/:id/title | F2.05 |
| 6 | DELETE /api/tasks/:id | F2.05 |
| 7 | POST /api/tasks/:id/duplicate | F2.05 |
| 8 | POST /api/tasks/:id/assets/extract | F2.06 |
| 9 | POST /api/tasks/:id/assets/extract2 | F2.06 |
| 10 | PUT /api/tasks/:id/extract-settings | F2.06 |
| 11 | GET /api/tasks/:id/full | F2.04 |
| 12 | GET /api/tasks/:id/full2 | F2.04 |
| 13 | POST /api/tasks/:id/mark-failed | F2.05 |
| 14 | POST /api/tasks/:id/restart | F2.05 |
| 15 | POST /api/tasks/:id/continue | F2.08 |
| 16 | POST /api/tasks/:id/scenes/auto-generate | F2.08 |
| 17 | POST /api/tasks/:id/scenes/auto-generate/cancel | F2.08 |
| 18 | GET /api/tasks/:id/scenes2 | F2.10 |
| 19 | PUT /api/tasks/:id/scenes/:groupNo | F2.10 |
| 20 | DELETE /api/tasks/:id/scenes/:groupNo | F2.10 |
| 21 | POST /api/tasks/:id/resplit | F2.07 |
| 22 | POST /api/tasks/:id/storyboard-batch | F2.08 |
| 23 | GET /api/tasks/:id/credit-records | F2.13 |
| 24 | POST /api/tasks/:id/library-apply/:libId | F2.13 |
| 25 | PUT /api/shots/:id | F2.11 |
| 26 | DELETE /api/shots/:id | F2.11 |
| 27 | POST /api/shots/:id/slots | F2.11 |
| 28 | DELETE /api/shot-images/:slotId | F2.11 |
| 29 | GET /api/shots/:id/histories | F2.11 |
| 30 | PUT /api/shots2/:id/params | F2.11 |
| 31 | POST /api/assets/:id/image/upload | F2.12 |
| 32 | POST /api/assets/:id/regenerate | F2.12 |
| 33 | POST /api/assets/:id/derive | F2.12 |
| 34 | POST /api/images/storyboard/generate | F2.09 |
| 35 | GET /api/asset-library | F2.13 |
| 36 | POST /api/asset-library | F2.13 |
| 37 | POST /api/asset-library/:id/share | F2.13 |
| 38 | DELETE /api/asset-library/:id | F2.13 |
38/38 对账 routes.go:324-361 与 PRD §4 M2。注:/tasks/:id/export/* 6 条共享前缀归 M4 分册(echo radix 静态段优先语义见 m04);director_shots 双表通路另一半归 M10。
DIF 新发现(本期取证,PRD §3/§4 未登记——入 99-附录 B):DIF-29-1 空列表 wire null vs 冻结 [](六读面)/DIF-29-2 extract v1 失败 502 文案金额用总额(冻结单价,退款额正确)/DIF-29-3 extract2 item 缺 variants 键/DIF-29-4 image_histories 前缀 si_ vs 冻结 ih_(ids.go:60 PrefixImageHistory 定义未用)/DIF-29-5 legacy/duplicate INSERT 18 列零值覆盖 DDL 默认/DIF-29-6 V2 计费 ceil vs 冻结 round(TextCost 正确实现并存未用)/DIF-29-7 V2 台账模型名可空串/DIF-29-8 resplit 回显原始 instruction/DIF-29-9 preview 台账 desc 缺「生成」+Mock label 偏差/DIF-29-10 duplicate 截 60+槽位条件复制/DIF-29-11 groupNo 非数字冻结 500 vs Go 200/DIF-29-12 bind 失败文案统一化/DIF-29-13 legacy 缺省标题「未命名」vs 冻结「未命名剧本」/DIF-29-14 V2 script_type/face_style 无二值化/DIF-29-15 token 估算 rune vs UTF-16/DIF-29-16 full fallback 拼装 5 键收窄。
4.7 - FSD M3 · 生成与素材
← 总则 | 附录 → | 上游:PRD §4 M3 实现落点:
backend/internal/domain/generation/(model/service/service_video/service_media/provider/repo)+cmd/server/generation_handlers.go+cmd/server/generation_assembly.go+cmd/server/routes.go:53-77(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:369-451/providers.js/assets.js/routes2.js:40-67,408-429/routes3.js:33-55,379-428仅差异对拍参照。
M3 生成与素材
19 端点全量。本域是计费唯一链路核心域(总则 §2.3):credits.Service.TryDeduct → generation.Service.SmartVideo/Service.Generate(provider 真实通道或 Mock)→ 失败由 generation 内部统一退款(RefundGenError 文案 + ClaimVideoRefund 认领防重),调用方不得重复退款。SSE video 一类事件四状态帧(processing/progress/done/failed,hub.go:21);Mock 资产链(SVG/WAV)为字节级移植、Mock 行为是契约的一部分。公开面 2 条(style-presets/faq),其余 amw。
F3.01 视频生成(generate / generate-v2)
对应 PRD:§4 M3 · POST /api/videos/generate、POST /api/videos/generate-v2 | 状态:实现 | 实现落点:generation_handlers.go:25-61、service_video.go:122-189/263-350 | UI:
/video(VideoView 生成表单)| 场景挂钩:无专门 e2e(E2E-GEN-02 间接前置依赖 standalone 面)
1 功能定义
分镜视频生成双形态:① 固定扁价 50(无 settings 键);⑩ 拉取+每秒动态价(clamp 1-15s)+resolution 白名单+响应多 model 键。Mock 同步返片,真实通道异步+轮询+SSE。
2 触发条件与前置状态
- 生成表单提交 → 两端点(Bearer);关联 shot 时前置:shot 存在且属主(先 404 后 402,service_video.go:123 注释冻结 index.js:374→376 同序)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| shot_id | JSON body | 否 | 非空→ShotForUser(shots JOIN tasks 属主)查无 404 shot not found |
| prompt | JSON body | 否 | 缺省取 shot.VideoPrompt |
| standalone | JSON body | 否 | ① 形态字段 |
| ratio | JSON body | 否 | ①:firstNonEmpty(p.Ratio,"16:9") 忽略 shot.ratio;⑩:firstNonEmpty(p.Ratio, shotRatio(shot), "16:9")(冻结 null 传 null——PRD 差异②已登记) |
| duration | JSON body | 仅⑩ | int64;0→shot.Duration→仍 0→5→clamp [1,15] |
| resolution | JSON body | 仅⑩ | 白名单 720p/1080p,白名单外回 shot.resolution,仍不在回 “1080p”(冻结原样透传 480p——PRD 差异②已登记) |
_ = c.Bind(&req) 忽略解析错误(handlers 层惯例);无显式 400 面。
4 处理流程
flowchart TD
A["POST /videos/generate(-v2)"] --> B["ShotForUser 404 先行"]
B --> C["计费:① CostVideo=50 恒定<br/>⑩ video_pull + per_second×secs"]
C -->|不足| D["402 积分不足"]
C --> E["TryDeduct type=video<br/>⑩「视频生成 %ds %s(拉取%d+每秒%d,%s,-%d)」"]
E --> F["submitVideo"]
F --> G{"Mock?(api_key 空)"}
G -->|是| H["MakeVideo SVG 落盘 → InsertVideo done<br/>LinkShotVideo → 同步返回 url"]
G -->|否| I["InsertVideo processing + LinkShotVideoKeep<br/>SubmitVideo 真实供应商"]
I -->|SyncURL 同步给片| J["go finishVideo(无 processing 帧)"]
I -->|异步| K["go pollLoop + SSE processing"]- 轮询节奏:首查 8s/步进 10s/上限 90 次/每 3 次 progress 帧(providers.js:164-187 镜像);完成谓词 status∈{succeeded,completed,success,done} 且 url 非空(或 url 非空 status 空,宽容);超时 90 次→「供应商任务超时(15分钟)(任务 X)」。
- 完成链:SetVideoDone+LinkShotVideo(video_done)+通知「视频生成完成」+SSE done;失败链:SetVideoFailed+SetShotStatus(failed)+Refund(ClaimVideoRefund 认领)+通知+SSE failed。
- ⑩ prompt 尾追
[resolution](service_video.go:176);standalone 恒 false(routes2.js:420)。
5 输出与结果状态
- Mock:
{id,status:"done",url,seconds,cost,resolution};⑩ 另多model键且置尾(videoV2MockResult 键序)。 - 真实:
{id,status:"processing",seconds,cost,resolution,note}(url 与 note 互斥:有 note 无 url / 有 url 无 note——service_video.go:89/:99 注释);⑩ 同序多model。 model值=billing 三链(settings active_model_video→model_endpoints→KV video_model→缺省「星芒视频引擎」)。
6 状态流转
videos.status:processing(真实分支插行)→ done(finishVideo)/failed(failVideo)/cancelled(F3.04);DDL 默认 'queued' 代码从不写。shots.status → video_done(LinkShotVideo)/failed。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| shot 不存在/非本人 | 404 | shot not found |
| 余额不足 | 402 | 积分不足 |
| Mock 落盘失败 | videoFailFast 退款 + 500 | 视频生成失败:<cause>(50 积分已退回) |
| 真实提交失败 | SetVideoFailed+SetShotStatus+退款(认领制) | 502 视频生成提交失败:<cause>(<cost> 积分已退回) |
| 轮询耗尽/供应商报失败 | 退款(认领制)+SSE failed+通知「视频生成失败」 | reason 截 80 |
8 权限与数据规则
- 权限:登录态;shot 属主经
JOIN tasks … WHERE t.user_id=?(repo.go:252-254);standalone 无属主对象。 - 数据规则:写
videos/shots/credit_records/notifications;videos.task_id空→wire “"(Go 空;冻结 null——测试断言 TaskID=="")。
F3.02 视频列表(videos / standalone-videos)
对应 PRD:§4 M3 · GET /api/videos、GET /api/standalone-videos | 状态:实现 | 实现落点:generation_handlers.go:64-85、repo.go:178-196 | UI:VideoView 历史列表/独立工具页| 场景挂钩:E2E-NAV-01(读面)
1 功能定义
两个视频列表读:全量(LIMIT 100)与独立生成子集(standalone=1)。
2 触发条件与前置状态
- 列表页/工具页加载 → 两端点(Bearer)。
3 输入与校验
无参数。
4 处理流程
- videos:
SELECT * FROM videos WHERE user_id=? ORDER BY created_at DESC LIMIT 100。 - standalone-videos:同式 +
AND standalone=1(E2E-GEN-02 前置造数时standalone:true必传——e2e 清单 :11)。
5 输出与结果状态
Video 行数组(键形见 F3.01 §6 前数据模型):id/user_id/task_id/shot_id/standalone/prompt/status/file_path/error/attempts/seconds/resolution/refunded/created_at;prompt/error 指针 nil→null;created_at LocalTime "2006-01-02 15:04:05"。
6 状态流转
无(纯读)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无视频 | 200 空数组/null(gorp nil slice) | — |
| 降级模式 | 503 | 数据库未配置 |
quirk:standalone/refunded wire 出 bool true/false(冻结 SQLite SELECT * 透传 1/0 数字——前端类型未声明零消费,DIF-29-17);ORDER BY created_at DESC 无次级键同秒抖动(冻结同构保真)。
8 权限与数据规则
- 权限:登录态;
user_id=?谓词。
F3.03 单场无损打标(videos/tag)
对应 PRD:§4 M3 · POST /api/videos/tag | 状态:实现 | 实现落点:generation_handlers.go:100-113、service_video.go:468-489 | UI:分镜卡「AI 打标」| 场景挂钩:无
1 功能定义
对单镜视频提示词追加一致性标签补丁,零写表(提示词 patch 由前端拿回自行保存);扣动态图片价。PRD :153「10×镜数」系计法误述——冻结/Go 均单镜单次扣(引用勿照抄)。
2 触发条件与前置状态
- 打标按钮 →
POST /api/videos/tag(Bearer);前置 shot 存在且属主。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| shot_id | JSON body | 是 | ShotForUser 查无 404 shot not found |
4 处理流程
TryDeduct(cost=billing.View().ImageCost settings image_generation 默认 10;type=tag;desc 单场无损打标(-10);taskId 传 shot id——冻结 index.js:391 照抄)→ 组装标签:transition_mode=="anchor_prev"→「锚定上一镜」否则「独立转场」。
5 输出与结果状态
{"tags":["主体一致","光影连续","运镜连贯",<tail>],"prompt_patch":"\n[一致性标签] 主体一致 / 光影连续 / 运镜连贯"}——prompt_patch 首字符为换行符(冻结模板字面量逐字)。
6 状态流转
无(零写表,service_video.go:468 注释)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| shot 非本人 | 404 | shot not found |
| 余额不足 | 402 | 积分不足 |
8 权限与数据规则
- 权限:登录态+shot 属主。计费价随 F6 settings(冻结恒 COSTS.image=10——PRD 差异④已登记)。
F3.04 生成取消(videos/:id/cancel)
对应 PRD:§4 M3 · POST /api/videos/:id/cancel | 状态:实现 | 实现落点:generation_handlers.go:88-97、service_video.go:453-465 | UI:VideoView cancelGen(confirm「供应商已产生费用,取消不退积分」→toast「已取消生成(积分不退)」)| 场景挂钩:无
1 功能定义
取消生成中的视频:置 cancelled+固定 error 文案,不退款、不中止轮询(其后 finishVideo/failVideo 仍可覆写终态——service_video.go:453-454 注释登记)。
2 触发条件与前置状态
- 生成卡取消按钮(confirm 前置)→
POST /api/videos/:id/cancel(Bearer)。
3 输入与校验
路径 :id;VideoByID(WHERE id=? AND user_id=?)查无 404 not found。
4 处理流程
SetVideoCancelled:UPDATE videos SET status='cancelled', error='用户取消(供应商已产生费用,积分不退)' WHERE id=?——不动 refunded(CancelErrorText model.go:93 冻结 routes2.js:428 逐字)。
5 输出与结果状态
{"cancelled":true,"refund":0}。
6 状态流转
processing → cancelled(但非终态锁——轮询 goroutine 仍可覆写 done/failed,登记于代码注释)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 不存在/非本人 | 404 | not found |
| 已终态再取消 | 仍 200 覆写 cancelled | — |
8 权限与数据规则
- 权限:登录态+属主;只写自己一行。
F3.05 视频配音(voiceover)
对应 PRD:§4 M3 · POST /api/videos/:id/voiceover、GET /api/videos/:id/voiceover | 状态:实现 | 实现落点:generation_handlers.go:252-278、service_media.go:155-184、repo.go:409-431 | UI:VideoView 配音面板| 场景挂钩:无
1 功能定义
Mock TTS:文本合成演示 WAV(零扣费),写 voiceovers 表;列表读最近 5 条。
2 触发条件与前置状态
- 配音面板提交/加载 → 两端点(Bearer);前置视频存在且属主。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| text | JSON body(POST) | 是 | TrimSpace 空 → 400 请输入配音旁白文本 |
| voice | JSON body(POST) | 否 | 白名单 female/male/child/narrator,白名单外静默回退 narrator |
4 处理流程
VideoByID 404 video not found → MakeVoiceoverWav(sr=16000,dur=min(4, 0.8+runeCount×0.02);base 频率 female 523/male 330/child 659/narrator 392)→ 落盘 tts_<uuid>.wav → InsertVoiceover 六列(id vo_ 前缀,status 走 DDL 默认 done)。列表:SELECT * FROM voiceovers WHERE video_id=? AND user_id=? ORDER BY created_at DESC LIMIT 5(查无返空数组无 404 面,冻结同)。
5 输出与结果状态
POST → {"id","url","voice","note":"演示 TTS 音(正式版接入真实配音模型)"}(冻结 routes3.js:412 逐字);GET → Voiceover 数组。
6 状态流转
voiceovers.status 恒 done(INSERT 不写该列)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 文本空 | 400 | 请输入配音旁白文本 |
| 视频非本人 | 404 | video not found |
| voice 非法值 | 静默 narrator | — |
8 权限与数据规则
- 权限:登录态+视频属主。
- 数据规则:WAV 时长用 rune 计数(JS
.length是 UTF-16 单元——astral 字符差异登记不设防,mockassets.go:93-94)。
F3.06 风格预设与 FAQ(style-presets / support/faq)
对应 PRD:§4 M3 · GET /api/style-presets、GET /api/support/faq(公开面,无 amw)| 状态:实现 | 实现落点:generation_handlers.go:193-202/281-283、service_media.go:194-207 | UI:风格选择器/FAQ 页| 场景挂钩:E2E-SNAP-01(间接)
1 功能定义
两个公开只读面:19 项风格目录(thumb SVG 幂等落盘)与 6 条静态 FAQ。FAQ 无 requireDB(唯一例外,冻结同);style-presets 有(降级 503——冻结零 DB 依赖恒 200,DIF-29-20)。
2 触发条件与前置状态
- 未登录可访问(pub 组,routes.go:58-59)。
3 输入与校验
无参数。
4 处理流程
- style-presets:遍历 StyleCatalog 19 项(cyberpunk→custom,顺序即响应序,与冻结 routes3.js:33-51 逐字一致)→ MakeStyleThumb 每次覆盖写
style_<id>.svg(冻结 existsSync 存在复用——确定性内容结果等价,mockassets.go:172 登记差异)。 - faq:静态 6 条
{q,a}(含「图片 10 积分/张;视频 = 拉取 5 + 每秒 5 积分」计费说明——冻结原文照抄,与 F6 覆盖价可能漂移系冻结固有)。
5 输出与结果状态
style-presets → [{id,name,group,thumb}]×19;faq → [{q,a}]×6。
6 状态流转
无。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 降级模式调 style-presets | 503 | 数据库未配置(DIF-29-20) |
| 降级模式调 faq | 200 正常 | — |
| thumb 落盘失败 | 500 | — |
8 权限与数据规则
- 权限:公开(无 amw 无 requireDB——faq);写入面仅文件系统(style SVG)。
F3.07 计价投影(models/active)
对应 PRD:§4 M3 · GET /api/models/active(v1.3 W10 补登归属 M3——「⑲ getBilling 投影」)| 状态:实现 | 实现落点:generation_handlers.go:286-291、generation_assembly.go:31-62、service_video.go:62-76、credits/billing.go:64-103 | UI:VideoView/DashboardView 计价展示(消费方表述两份文档不一致:PRD :155 称 DashboardView、API-增量.md:65 称 VideoView——B 类漂移登记,留勘误)| 场景挂钩:无
1 功能定义
当前计费配置投影:价格四键(settings 可调)+ 模型三链解析结果——前端计价展示与费用估算的唯一数据源。
2 触发条件与前置状态
- 前端挂载拉取 →
GET /api/models/active(Bearer)。
3 输入与校验
无参数。
4 处理流程
- 价格四键:settings
image_generation/video_pull/video_per_second/text_tokens_per_credit(默认 10/5/5/1000);非数字回退默认(冻结+getSettingNaN 继续传播——billing.go:49-50 登记差异)。 - 模型三链
modelByPurpose:settingsactive_model_<p>→ model_endpoints 行(purpose+enabled=1)→ KV<p>_model→ 中文缺省「星芒文本/图像/视频引擎」;image_fallback无 active 链仅 KVimage_model_fallback→「星芒图像引擎 · 备选」。 - settings 表
key为 MySQL 保留字须反引号(task_assembly.go:33-35 注释载十五续修复的存量 bug)。
5 输出与结果状态
{"image_cost":N,"video_pull":N,"video_per_second":N,"text_tokens_per_credit":N,"models":{"text","image_primary","image_fallback","video"}}(BillingView 键序)。
6 状态流转
无(读投影)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 降级模式 | 503 | 数据库未配置 |
| settings 值非数字 | 回退默认(冻结 NaN 传播——登记差异) | — |
8 权限与数据规则
- 权限:登录态(冻结 authRequired routes2.js:67 同)。
F3.08 图片素材(images 五端点)
对应 PRD:§4 M3 · GET /api/images、POST /api/images/upload、POST /api/images/generate、GET /api/images/:id、DELETE /api/images/:id | 状态:实现 | 实现落点:generation_handlers.go:116-190、service.go:83-141、service_media.go:23-92、repo.go:19-46/306-369 | UI:
/tools(IndependentToolsView)| 场景挂钩:E2E-GEN-01(generate Mock 同步)、E2E-CRD-01(台账「独立图片生成(-10)」)
1 功能定义
独立图片素材五动作:standalone 列表、上传(72h 过期)、生图(动态价+可选参考图)、单张重定向读(query token 兜底两处之一)、删除(恒 ok)。
2 触发条件与前置状态
- 工具页操作 → 五端点(Bearer);
GET /images/:id另支持 query token(C3:FromRequest Bearer 优先+query 兜底;冻结 index.js:426-427 query 优先且路由级无 authRequired——差异已登记 routes.go:72)。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| GET /images | — | 无 |
| POST /upload | multipart file+form ratio/prompt | data==nil → 400 缺少文件(冻结 index.js:405) |
| POST /generate | {"prompt","ratio","ref_image_ids":[]} | TrimSpace(prompt) 空 → 400 请输入图片提示词(先校验后扣费,冻结同序) |
| GET /:id | — | 查无 404 not found |
| DELETE /:id | — | 0 行也 200(恒 ok,repo.go:102 注释) |
4 处理流程
flowchart TD
A["POST /images/generate"] --> B["prompt 非空校验"]
B --> C["TryDeduct type=image task_id=''<br/>「独立图片生成(-10)」"]
C -->|不足| D["402"]
C --> E["Service.Generate:真实通道 GenerateImage<br/>或 Mock MakeImage SVG"]
E -->|失败| F["Refund claim=nil + 502<br/>「图片生成失败:<cause>(10 积分已退回)」"]
E --> G["InsertImageStandalone kind=standalone<br/>expires_at=now+72h"]
G --> H["ref_image_ids 非空→prompt 尾追 ' [参考图N张]'"]- 真实通道
provider.GenerateImage(provider.go:129-180):POST/images/generations(model/prompt 截 4000/n:1/size 按 ratio 9:16→1024x1536、1:1→1024x1024、默认 1536x1024);180s 超时「图片生成超时」;非 200HTTP <code>:<msg截200>;data 空「接口未返回图片数据,请核对模型名」;b64 解码 ext=.png、url 下载 ext 由扩展捕获组不归一化;两者皆无「图片接口返回格式未知(无 url/b64_json)」——失败原样上抛。 - upload:ext 空补 .png;落盘
up_文件名/img_行 id 怪形态(冻结 saveUpload 保留);origin_name 过 FixFilename;prompt 缺省上传原图 <名>。 - GET /:id:
SELECT file_path WHERE id=? AND user_id=?→ 302 重定向(冻结 res.redirect 默认 302 同)。
5 输出与结果状态
- 列表 → StandaloneImage 数组(
expires_atISO 带 T/Z 或 null——C7 例外全达成,PRD 差异①)。 - upload →
{"id","url"}(无 expires_at 键);generate →{"id","url","expires_at"};delete → data true。
6 状态流转
无独立状态机;images 行插入即终态(expires_at 72h 后逻辑过期,无清理任务)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 缺文件 | 400 | 缺少文件 |
| prompt 空 | 400 | 请输入图片提示词 |
| 余额不足 | 402 | 积分不足 |
| 生成失败 | 退款(claim=nil 无防重面——images 同步路径照冻结) | 502 图片生成失败:<cause>(<cost> 积分已退回) |
| 删不存在 | 200 true | — |
| ref 追加失败 | _ = 吞错(冻结同语句抛错进中央 500——DIF-29-21) | — |
8 权限与数据规则
- 权限:登录态;读删谓词
user_id=?;upload/generate 无跨表属主。 - 数据规则:写
images/credit_records;up20 为请求体总量限额(冻结 multer 单文件 20MB——M9 节同族差异引用不重立)。
F3.09 音频素材(audios 三端点)
对应 PRD:§4 M3 · GET /api/audios、POST /api/audios、DELETE /api/audios/:id | 状态:实现 | 实现落点:generation_handlers.go:205-249、service_media.go:112-139、repo.go:371-407 | UI:VideoView 配音素材区/独立工具页| 场景挂钩:无
1 功能定义
音频素材三动作:列表(按 task 过滤双分支)、上传(白名单扩展名)、删除(恒 ok)。全部零扣费。
2 触发条件与前置状态
- 素材区操作 → 三端点(Bearer);上传限额 50MB(up50)。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| GET /audios | query task_id | 无 task_id→全量 LIMIT 100 |
| POST /audios | multipart file+form name/task_id | data==nil → 400 缺少音频文件;ext(小写)不在 .mp3/.wav/.m4a/.aac/.ogg/.flac → 400 仅支持 mp3/wav/m4a/aac/ogg/flac(先白名单后落盘,冻结同序) |
| DELETE /audios/:id | — | 恒 ok |
4 处理流程
上传:Upload 落盘(up_ 文件名形态)→ InsertAudio(id au_ 前缀——routes3.js:391,P0 误写 aud_ 已勘误)→ name=truncate(firstNonEmpty(name,fileName),60);task_id 空串落 ''(冻结落 NULL——DDL NOT NULL 适配,repo.go:113 注记)。列表双分支:有 task_id→WHERE user_id=? AND (task_id=? OR task_id='') 无 LIMIT(冻结 task_id IS NULL 适配);无→LIMIT 100。
5 输出与结果状态
列表 → Audio 数组(id/user_id/task_id/name/file_path/size/created_at);上传 → {"id","url"};删除 → data true。
6 状态流转
无。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 缺音频文件 | 400 | 缺少音频文件 |
| 扩展名非法 | 400 | 仅支持 mp3/wav/m4a/aac/ogg/flac |
| 删不存在 | 200 true | — |
8 权限与数据规则
- 权限:登录态;谓词
user_id=?。 - 数据规则:
contentTypeOf缺.mp3/.m4a/.../.mp4分支——音频上传 Content-Type 落 defaultimage/svg+xml(功能面 URL 直读不受影响,wire 头形差异 DIF-29-18)。
M3 端点覆盖对账
| # | 端点 | 功能节 |
|---|---|---|
| 1 | POST /api/videos/generate | F3.01 |
| 2 | GET /api/videos | F3.02 |
| 3 | POST /api/videos/tag | F3.03 |
| 4 | POST /api/videos/generate-v2 | F3.01 |
| 5 | POST /api/videos/:id/cancel | F3.04 |
| 6 | POST /api/videos/:id/voiceover | F3.05 |
| 7 | GET /api/videos/:id/voiceover | F3.05 |
| 8 | GET /api/standalone-videos | F3.02 |
| 9 | GET /api/models/active | F3.07 |
| 10 | GET /api/images | F3.08 |
| 11 | POST /api/images/upload | F3.08 |
| 12 | POST /api/images/generate | F3.08 |
| 13 | GET /api/images/:id | F3.08 |
| 14 | DELETE /api/images/:id | F3.08 |
| 15 | GET /api/audios | F3.09 |
| 16 | POST /api/audios | F3.09 |
| 17 | DELETE /api/audios/:id | F3.09 |
| 18 | GET /api/style-presets | F3.06 |
| 19 | GET /api/support/faq | F3.06 |
19/19 对账 routes.go:58-77 与 PRD §4 M3。已登记差异引用:PRD 差异①(expires_at C7)②(resolution/ratio 收紧向)③(SSE progress 时序 fetch 后)④(tag 动态价);POST /images/storyboard/generate 归 M2(PRD :150)。
DIF 新发现(本期取证,入 99-附录 B):DIF-29-17 videos standalone/refunded wire bool vs 冻结 1/0(前端零消费)/DIF-29-18 contentTypeOf 缺音频/视频扩展分支(Content-Type 落 svg)/DIF-29-19 Mock 落盘文件名形态(label+ObjectKey vs 冻结 img_<uid> 行 id 即文件名)/DIF-29-20 style-presets 降级 503(冻结零 DB 依赖)/DIF-29-21 ref 追加错误吞没(冻结进中央 500)/DIF-29-22 真实通道 Link 失败 failFast 退款(冻结 try 外不退——integration_test.go:320-323 裁决范围收窄注记引用)。
4.8 - 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 处理流程
flowchart TD
A["①GET /export/task/:taskId"] -->|404| B["②POST /export"]
A -->|命中| E["③轮询"]
B --> C["TaskOwner 属主"]
C -->|无| D["404 task not found"]
C --> F["CountShotsWithVideo:video_id != ''"]
F --> G["TryDeduct n×2 type=export<br/>「导出成片 N 段(-2n)」"]
G -->|不足| H["402 积分不足"]
G --> I["INSERT export_tasks<br/>status=complete progress=100 shot_count=n file_path=NULL"]
I --> E- ① 查重响应:
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 处理流程
flowchart TD
A["POST render-all"] --> B["TaskOwner 404 面"]
B --> C["ListShotsNoVideo:video_id = '' ORDER index_no"]
C -->|空| D["200 {rendered:0} 不扣费"]
C --> E["Σ VideoCost(secs=duration‖5) 一次 TryDeduct<br/>「批量极速渲染 ×N(-Σ,模型名)」type=video"]
E -->|不足| F["402"]
E --> G["逐镜串行 RenderOne"]
G -->|err| H["failed++ continue(退款在 generation 内认领制)"]
G -->|processing| I["processing++"]
G --> J["note 两段:N 段失败已退款;N 段已提交供应商队列…"]- 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 正文未含)。
4.9 - FSD M5 · 通知公告
← 总则 | 附录 → | 上游:PRD §4 M5 实现落点:
backend/internal/domain/notification/(service.go 读侧 89 行+hub.go SSE 基座)+cmd/server/notify_handlers.go+cmd/server/routes.go:96-102(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:474-485仅差异对拍参照。 域内要点:通知零 SSE(hub.go 属本包但通知读面不走事件——写入方五源自持 INSERT);** announcements 公开面无 amw**;read是 MySQL 保留字全 SQL 反引号(PRD ①);对账勿混:routes.go:288-290/admin/announcements*3 条属 M12 管理后台组。
M5 通知公告
4 端点全量(读 3+公开 1;写侧散布五域:auth/admin/recharge/seed/evaluation 自持 INSERT notifications——本域零写通知)。PRD §4 M5 已登记差异 ①~④(prd-qmtv-v1.md:168)引用不重立。
F5.01 通知读面与已读(notifications 三件)
对应 PRD:§4 M5 · GET /api/notifications、GET /api/notifications/unread-count、POST /api/notifications/read | 状态:实现 | 实现落点:notify_handlers.go:13-52、service.go:44-79 | UI:App.vue 顶栏铃铛(60s 轮询 unread-count)+下拉列表 | 场景挂钩:无专门 e2e(E2E-NAV-01 间接;集成锚 WorkbenchNotifyChain 第十二链覆盖 unread-count 联动+read 双分支)
1 功能定义
站内通知读面:最近 50 条列表、未读计数、已读标记(单条/全标)。
2 触发条件与前置状态
三端点登录态(amw);零前置(无通知返回空/0)。
3 输入与校验
list 无参数;read body {id?} 可选——有 id 单条/无 id 整用户全标(service.go:69-77);id 不属于自己也静默 0 行(无 404 面,quirk 照抄冻结 :480-483)。
4 处理流程
list:SELECT id,user_id,title,content,\read`,created_at … WHERE user_id=? ORDER BY created_at DESC LIMIT 50(:53-55——**无已读过滤、无分页**,quirk;read 反引号 PRD ①)。unread-count:COUNT(*) … AND `read`=0(:63-65)。read:单条 WHERE id=? AND user_id=?/ 全标WHERE user_id=?`(:69-77)。
5 输出与结果状态
list=NotificationRow 数组(read int64 0/1、Content *string 可空下发 null,service.go:20-27);unread-count=ok 裸数字(handler OK(c, n),非对象信封——quirk);read 恒 true。
6 状态流转
read 0→1 单向(无未读回置——通知一旦已读不可逆,照冻结)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 超过 50 条 | 旧通知不可见(LIMIT 硬截,无分页参数) | — |
| read id 非本人/不存在 | 200 恒 true 静默 0 行(quirk) | — |
| bind 失败/空 body | 等价全标已读(_ = c.Bind 静默,notify_handlers.go:45——与 DIF-29-23 同族形态,M9 DIF-30-2 同注) | — |
8 权限与数据规则
authed + amw 全部按 token user_id 隔离(越权静默不暴露存在性);写入方五源形态兼容(content 可空 TEXT)。
F5.02 公告公开面(announcements/active)
对应 PRD:§4 M5 · GET /api/announcements/active ○ | 状态:实现 | 实现落点:notify_handlers.go:53-63、service.go(announcements 段)+routes.go:102(pub 组无 amw)| UI:App.vue 全局公告条 | 场景挂钩:E2E-NAV-01(公开路由巡检覆盖)
1 功能定义
启用中公告列表(管理台 M12 写侧的公开读镜像)。
2 触发条件与前置状态
公开面无鉴权(routes.go:102 pub 组——PRD ③;冻结 index.js:485 无 authRequired 同形)。
3 输入与校验
无参数。
4 处理流程
SELECT id, title, content, active, created_at FROM announcements WHERE active=1 ORDER BY created_at DESC LIMIT 10(显式 5 列非 SELECT *;LIMIT 10 硬截——冻结 index.js:485-487 同;active int64 1/0)。
5 输出与结果状态
AnnouncementRow 数组(id/title/content/active/created_at 蛇形,service.go:30-36);最多 10 条(LIMIT 10 硬截,service.go:87)。
6 状态流转
active 由 M12 admin 面翻转(本域只读)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无启用公告 | 空数组(nil→[],DIF-29-1 同族形态) | — |
| 启用公告 >10 条 | 仅返回最近 10 条(LIMIT 10 硬截,冻结 index.js:487 同) | — |
| 未登录访问 | 200 正常返回(公开面) | — |
8 权限与数据规则
公开面零鉴权零属主;content 全文下发;admin 写侧三条路由(:288-290)归 M12——本册对账不含。
册尾端点覆盖对账(4/4)
对账 backend/cmd/server/routes.go:96-102 亲数 4 条 ↔ 本册 F5.01~F5.02:
| # | 端点(routes.go:line) | 节 |
|---|---|---|
| 1 | GET /notifications(:99) | F5.01 |
| 2 | GET /notifications/unread-count(:100) | F5.01 |
| 3 | POST /notifications/read(:101) | F5.01 |
| 4 | GET /announcements/active(:102,pub 无 amw) | F5.02 |
差异纪律:PRD §4 M5 ①~④ 全引用不重立;本册零新立 DIF(bind 静默已随 DIF-30-2 族注记;quirk 均双方一致照抄)。
4.10 - FSD M6 · 组织协同
← 总则 | 附录 → | 上游:PRD §4 M6 实现落点:
backend/internal/domain/org/(model 308/service 775/repo 209/repo_read 389/repo_groups 55/perms 53/code 24 单包多文件)+cmd/server/org_handlers.go(460 行 29 handler)+cmd/server/routes.go:103-136(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:513-597(组织段 9)+enterprise.js(20)仅差异对拍参照。 域内要点:四零域(零上传/零扣费/零 SSE/零通知,无 BodyLimit——routes.go:105-106);无组织分流四态谱:me·dashboardok(null)|folders·groups·perm-groups·material-perms·shared-projectsok([])|folder create·assign·folder delete 400「您尚未加入企业」|allocation-mode 404「未加入组织」——逐端点归属各异勿统一;owner-严格谱系(invites/remove/role/groups/perm-groups/assign 六面 ownerOnly vs material-perms 唯一 adminOrOwner 面);「管理员」文案 vs owner 严格判定(PRD ⑩);projectRank/memberRank驼峰 wire 双键(同 M2 shotCount 族勿修正);permGroups/materialPerms 响应经 map 重出(permsjson:"-"串解析为数组)。PRD §4 M6 已登记差异 ①~⑪+十九续勘正批注(prd-qmtv-v1.md:172-178)引用不重立。
M6 组织协同
29 端点全量(组织面 9+enterprise 面 20,两口径重合;#8 share 物理路由冻结在 index.js:574 非 director.js——routes.go:106-107 归组注记)。
F6.01 组织申请、档案与分账模式(applications/me/allocation-mode)
对应 PRD:§4 M6 · POST /api/organizations/applications、GET /api/organizations/me、PUT /api/organizations/me/credit-allocation-mode | 状态:实现 | 实现落点:org_handlers.go:18-68、service.go:59-106、repo.go:128-132/:178-181、repo_read.go:16-54 | UI:EnterpriseApplyView.vue(申请)/OrganizationView.vue(档案)/分账模式前端零消费 | 场景挂钩:无专项 e2e(OrgChain 第九链集成锚替位;E2E-NAV-01 间接)
1 功能定义
提交建组织申请、读取本人组织档案四联文档(组织+成员+邀请+积分流水)、切换组织积分分账模式(死端点照抄)。
2 触发条件与前置状态
申请仅登录无权限判定(重复申请不查重双侧同形);me 无组织返 null 非 404(service.go:72-74→handlers:46-48);allocation-mode 经 orgOf 判定,无成员行 → 404「未加入组织」(service.go:98——四态谱第四态独一份),无 role 门槛 member 可改。
3 输入与校验
申请 body company/contact/reason(company 空 → 400「公司名称必填」service.go:61;三者不 slice 不 trim——差异⑤ VARCHAR(128) strict 溢出报错 vs 冻结 SQLite 照收);allocation-mode body mode 可选缺省 “"。
4 处理流程
申请:oa_ id(ids.go:63)→ InsertOrgApplication 五列单写,status 走 DDL 默认 ‘pending’(repo.go:128-132)。me:OrgDetail 四查串行零写——组织+成员(repo_read.go:16-35)→invites(:38-43,无 status 过滤 pending+used 全回 quirk)→credit_records(:46-54,LIMIT 50 task_id 尾插)。分账:UPDATE organizations SET credit_allocation_mode=?,mode 非 independent 一律归 pooled(service.go:102-104——死端点:前端零消费实证 grep allocation 零命中,PRD ①)。
5 输出与结果状态
申请 {"id"}(oa_);me 无组织 data:null,有组织 MeDoc 键序 id,name,owner_id,status,credit_allocation_mode,credits,created_at,my_role,members,invites,credit_records(model.go:204-222);members 行 id,org_id,user_id,role,quota,group_id,perm_group_id,material_perms,created_at,email,nickname(group_id/perm_group_id NULLIF 还原 null、material_perms 为 JSON 字符串形态非数组、email/nickname omitempty);分账 {"mode"}。
6 状态流转
org_applications 插行(审批写侧归 M12 F12.09);organizations.credit_allocation_mode 单列覆写(差异⑦:冻结无读写消费方空转照抄)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| company 空 | 400 | 「公司名称必填」 |
| me 无组织 | 200 | data:null(四态谱第一态) |
| allocation 无成员行 | 404 | 「未加入组织」(冻结 index.js:530 同 404 非 400) |
| bind 失败 | 400 | err.Error() 原文(echo 库消息,14 处同制) |
8 权限与数据规则
quirk:分账端点无 role 门槛(member 可改组织级配置);冻结侧差异①登记死端点照抄。
F6.02 邀请与加入(invites/join)
对应 PRD:§4 M6 · POST /api/organizations/me/invites、POST /api/organizations/join | 状态:实现 | 实现落点:org_handlers.go:71-103、service.go:110-142、code.go:14-24、repo.go:135-157 | UI:OrganizationView.vue(成员 tab)| 场景挂钩:OrgChain(invite→join→三哨兵)
1 功能定义
owner 生成 8 位邀请码(crypto/rand 大写 base36——差异⑥形态等价);凭码加入组织(三写禁事务)。
2 触发条件与前置状态
invites:orgOf 无行或非 owner → 双分支同哨兵 ErrOwnerOnlyInvite 403(service.go:113-120——无组织并入 403 非 404)。join 仅登录;码查 code=? AND status='pending' 无行 → 400;无「已入组」防重(双侧同形,重复 join 产生重复 org_members 行)。
3 输入与校验
invites body email 可选缺省 “"(无格式校验双侧同形);join body code 可选缺省 “"。
4 处理流程
invites:genInviteCode(code.go:14-24)→ inv_ id → InsertInvite 四列单写。join 三写逐条禁事务(service.go:127-142=冻结 :546-548 同序):MarkInviteUsed(status→‘used’)→ InsertOrgMember(om_,role 恒 ‘member’)→ SetUserOrg(users.org_id 回填);任一失败即中断前写不回滚(三条铁律之三)。
5 输出与结果状态
invites {"id","code"};join data:true。
6 状态流转
org_invites pending→used;org_members 新增 member 行;users.org_id ← inv.org_id(跨三表写序固定)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非 owner/无组织 | 403 | 「仅组织管理员可邀请」(文案「管理员」vs 判定 role=='owner' 严格——PRD ⑩) |
| 码无效/已用 | 400 | 「邀请码无效或已使用」 |
8 权限与数据规则
ownerOnly 严格;join 无防重 quirk 照抄(幂等性缺失=冻结原样)。
F6.03 成员移除与角色(members DELETE/role PUT)
对应 PRD:§4 M6 · DELETE、PUT /api/organizations/me/members/:memberId(/role) | 状态:实现 | 实现落点:org_handlers.go:106-132、service.go:146-198、repo.go:114-124/:160-175 | UI:OrganizationView.vue(成员 tab)| 场景挂钩:OrgChain(移除双态合并断言)
1 功能定义
owner 移除成员(两写:删行+users.org_id 清空)、调整成员角色(非 ‘admin’ 一律归 ‘member’ 双态收敛)。
2 触发条件与前置状态
两端点 orgOf 无行/非 owner → 各自同哨兵 403;role 面 target 无行 → 404「成员不存在」(service.go:186-192);remove 面 target 查 MemberByID(org 隔离 WHERE id=? AND org_id=?)。
3 输入与校验
路径 memberId;role body role 可选(service.go:196-198 非 admin 归 member——与 allocation-mode 同构双态收敛)。
4 处理流程
remove:target.Role==‘owner’ → 400「无法移除」→ 两写 DeleteOrgMember + ClearUserOrg(UPDATE users SET org_id=''——冻结 SET org_id=NULL 的 Go 空串语义等价,repo.go:159 注)。role:owner target 拒 → 归一 → SetMemberRole 单写。
5 输出与结果状态
remove data:true;role {"id","role"}(归一后值)。
6 状态流转
org_members 删行/role 覆写;users.org_id 清空(’’)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| target 不存在 或 target 是 owner | 400 | 「无法移除」(双态合并同一哨兵 ErrCannotRemove——PRD ⑨,非 404 quirk) |
| role target 无行 | 404 | 「成员不存在」 |
| role target 是 owner | 400 | 「不能修改所有者角色」(文案「所有者」与 invites「管理员」不同源——冻结 :564 vs :537 原样差异) |
| 非 owner | 403 | 「仅组织所有者可调整角色」 |
8 权限与数据规则
owner 无法被移除也无法被改角色(owner 调用时 target 即 owner);org 隔离谓词防跨组织 memberId 枚举。
F6.04 项目共享与共享列表(share/shared-projects)
对应 PRD:§4 M6 · POST /api/director3d/:id/share、GET /api/organizations/shared-projects | 状态:实现 | 实现落点:org_handlers.go:135-165、service.go:203-232、repo.go:196-209、repo_read.go:167-180 | UI:Director3DView.vue(分享开关)/OrganizationView.vue(共享列表)| 场景挂钩:OrgChain(share/search 面);E2E-NAV-01 间接
1 功能定义
导演项目所有者切换 org_shared 开关(跨 /director3d 前缀归本域——冻结 index.js:574);组织内读取共享项目列表(两级子查询计数+全剧本下发)。
2 触发条件与前置状态
share:判定序属主先于组织(ProjectOwned 不属主 → 404 英文 “not found”;再 orgOf 无行 → 400「您尚未加入企业,无法共享」——冻结 :575-578 同序);shared-projects:orgOf 无行 → ok([]) 非 404(四态谱第二态)。
3 输入与校验
share body shared bool(冻结 truthy 强转 ? 1 : 0 vs Go bool 严格 bind);shared-projects 无输入。
4 处理流程
share:存在性 COUNT → orgOf → UPDATE director_projects SET org_shared=? 单写(repo.go:202-209)。shared-projects:dp. 显式 10 列+owner_name/owner_email 尾插+episodes/videos 两级 COUNT 子查询(video_id IS NOT NULL 即止);WHERE org_shared=1 AND user_id IN(...);ORDER BY created_at DESC 无 LIMIT。
5 输出与结果状态
share {"id","org_shared"}(值 int64 0/1 数值非 bool);shared-projects=ShareRow 数组,行键 id,user_id,name,style_id,face_style,status,kind,org_shared,script_text,created_at,owner_name,owner_email,episodes,videos(model.go:110-125——script_text 全剧本下发即冻结 dp.* 语义,十九续 C1 投影补齐)。
6 状态流转
director_projects.org_shared ← 0|1。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非属主/项目不存在 | 404 | “not found”(英文原样,ErrNotFoundEN 共享哨兵) |
| 未入组织 | 400 | 「您尚未加入企业,无法共享」 |
8 权限与数据规则
仅属主可共享本人项目(orgOf 仅判存在性——member 亦可共享);member 可见全组织共享面。
F6.05 团队看板(dashboard)
对应 PRD:§4 M6 · GET /api/organizations/me/dashboard | 状态:实现 | 实现落点:org_handlers.go:167-178、service.go:240-317、repo_read.go:57-138 | UI:OrganizationView.vue(团队看板 tab)| 场景挂钩:OrgChain(available==balance 钉死断言)
1 功能定义
组织级积分总览(五聚合)+近 14 天消耗趋势+项目/人员双排行(全站最密 quirk 节)。
2 触发条件与前置状态
orgOf 无行 → data:null(四态谱第一态);无 role 门槛。
3 输入与校验
无输入(GET 零参数)。
4 处理流程
MemberIDsOfOrg → 五聚合逐查(balance/recharged=approved 充值/consumed=负额取反/frozen=eval_projects status=‘running’ SUM)→ trend 14 天循环(i:13→0,SQL 方言 DATE(DATE_SUB(NOW(), INTERVAL ? DAY))——repo_read.go:5-6 冻结 date('now',?,'localtime') 等价改写)→ projectRank TOP10(谓词 task_id IS NOT NULL——十九续 A4 勘误还原,空串聚合行「独立生成 / 工具」上榜)→ memberRank TOP20(name=nickname‖email)。
5 输出与结果状态
DashboardDoc 键序 overview,trend,projectRank,memberRank;overview 键序 balance,available,frozen,recharged,consumed;行键 rank,name,kind,amount(kind 单词键——十九续 A1 曾误写 amount-kind 连字符已修,wire 键集断言守卫自此入惯例)与 rank,name,email,amount;trend 行 label,amount。projectRank/memberRank 驼峰 wire 双键(同 M2 shotCount 族照抄勿修正)。
6 状态流转
纯读零写。
7 边界与异常
仅 401/503/ok(null) 面;空组织 ids → 聚合占位符空串照常返 0(repo_read.go:71-76)。
8 权限与数据规则
available==balance 谎言钉死(service.go:312 Available: balance 直接复制无独立查询——PRD ②);projectRank dir_ 死分支(service.go:287-290 HasPrefix “dir_” 永不命中——实产 dpr_(ids.go:25)且 TaskID[4:] 偏移随前缀长度错位,PRD ② 照抄);frozen 公式=M11 语义同形(M11 已落地后 running 可非零——「语义冻结谎言」勿写「恒 0」时态断言)。
F6.06 作品墙(showcase 三件)
对应 PRD:§4 M6 · GET/POST /api/showcase、DELETE /api/showcase/:id | 状态:实现 | 实现落点:org_handlers.go:182-223、service.go:320-346、repo_read.go:184-218 | UI:WorkShowcaseView.vue | 场景挂钩:E2E-NAV-01 间接;OrgChain(showcase 恒 ok 断言)
1 功能定义
全站作品展示列表(跨组织跨用户)、发布 done 成片为作品、删除本人作品。
2 触发条件与前置状态
列表仅登录无组织判定(全表);发布前置 VideoDoneOfUser(id=? AND user_id=? AND status='done' 三谓词)无行 → 404;删除无任何前置校验。
3 输入与校验
发布 body video_id(语义必填缺省 "” 查无 404)+title 可选(>60 rune 截 60、空串缺省「未命名作品」——冻结 String(title||'未命名作品').slice(0,60) 同形)。
4 处理流程
列表 Works 单读(7 列+author 尾插,ORDER BY created_at DESC LIMIT 60 跨组织全表);发布 wk_ id → InsertWork 五列单写(cover_path 不写走 DDL 默认 ‘’);删除 DELETE ... WHERE id=? AND user_id=? 不查行数。
5 输出与结果状态
WorkRow 行键 id,user_id,title,video_id,cover_path,video_path,created_at,author(cover_path *string NULLIF 还原 null);发布 {"id"};删除 true。
6 状态流转
works 插行(video_id/video_path 冗余固化自 videos)/删行。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非本人/未完成/不存在成片 | 404 | 「成片不存在或未完成」(三态合并单哨兵) |
| 删除他人/不存在作品 | 200 | 恒 true 静默 0 行(恒 ok quirk PRD ⑧) |
8 权限与数据规则
列表不分组织不看本人(冻结 e.js:62 无 WHERE quirk 照抄);删除 user_id=? 谓词限本人——他人作品删不掉只是不报。
F6.07 素材夹目录树(folders 三件)
对应 PRD:§4 M6 · GET/POST /api/organizations/me/material/folders、DELETE …/folders/:id | 状态:实现 | 实现落点:org_handlers.go:224-265、service.go:349-407、repo_read.go:222-251 | UI:OrganizationView.vue(素材 tab)| 场景挂钩:OrgChain(folders 面)
1 功能定义
组织素材夹目录树(含计数)读、建、删(先摘后删)。
2 触发条件与前置状态
GET 无行 → ok([])(四态谱第二态);POST 无行 → 400「您尚未加入企业」(四态谱第三态——冻结 e.js:89 同 400);DELETE 无行 → 400,夹不存在 → 404 英文。
3 输入与校验
POST body name(trim 必填 400、>30 rune 截 30)+type 可选——六类白名单 character,scene,prop,image,video,audio,白名单校验先于 name 必填判定(service.go:374-376 先于 :377),不在白名单缺省 ‘character’。
4 处理流程
GET AssetFolders 单读(6 列+asset_count 子查询,ORDER BY type,created_at);POST fd_ id → 五列单写;DELETE 两写先摘后删:DetachFolderAssets(UPDATE asset_library SET folder_id=''——冻结 NULL 的 ’’ 语义)→ DeleteFolder(DELETE by id 无 org 谓词——id 已隔离校验过,冻结 :103 同形)。
5 输出与结果状态
AssetFolderRow 行键 id,org_id,user_id,type,name,created_at,asset_count;POST {"id"};DELETE true。
6 状态流转
asset_library.folder_id 批量清空 + asset_folders 删行(两写无事务)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 未入组织(POST/DELETE) | 400 | 「您尚未加入企业」 |
| name 空 | 400 | 「文件夹名称必填」 |
| 夹不存在 | 404 | “not found”(英文共享哨兵) |
8 权限与数据规则
任何组织成员皆可建夹/删夹(无 owner 门槛——删除他人所建夹亦放行只要同组织,冻结同形)。
F6.08 素材列表与配额估算(materials)
对应 PRD:§4 M6 · GET /api/organizations/me/materials | 状态:实现 | 实现落点:org_handlers.go:266-277、service.go:411-449、repo_read.go:254-299 | UI:OrganizationView.vue(素材 tab)| 场景挂钩:OrgChain
1 功能定义
组织共享素材列表(type/folder 双可选过滤)+存储配额估算法(80KB/2MB 系数硬编码)。
2 触发条件与前置状态
orgOf 无行 → {assets:[],storage:null}(非 404 非 400——四态谱第二态变体)。
3 输入与校验
query type(!="" && !="全部" 才过滤)+folder_id(非空才过滤)——双可选。
4 处理流程
动态 SQL(显式 9 列+三 JOIN 尾插 image_url/owner_name/folder_name,WHERE user_id IN(...) AND org_shared=1,LIMIT 200)→ 存储三查(图片 SUM+NULL 计数双列/音频 SUM/视频 COUNT)→ 估算:used=img.Sum+img.Nulls×80KB+aud+vidCount×2MB,quota=10GB,percent=min(100, Round(×100×10)/10)(toFixed(1) 正数域等价——差异④)。
5 输出与结果状态
{assets,storage};AssetRow 行键 id,user_id,name,type,description,image_id,org_shared,folder_id,created_at,image_url,owner_name,folder_name(description/folder_id/folder_name/image_url NULL→null);storage 键序 used,quota,percent,plan,plan 恒「基础档」。
6 状态流转
纯读零写。
7 边界与异常
无业务异常面(仅 401/503)。
8 权限与数据规则
配额系估算法非实测(80KB/2MB 假设值——PRD ④);素材面=组织全员 org_shared=1 资产(含他人);视频按条数不按字节估。
F6.09 素材归夹(material/assign)
对应 PRD:§4 M6 · POST /api/organizations/me/material/assign | 状态:实现 | 实现落点:org_handlers.go:278-295、service.go:453-471、repo_read.go:302-311 | UI:OrganizationView.vue(素材 tab)| 场景挂钩:OrgChain
1 功能定义
本人素材移入/移出文件夹(folder_id 可空清除)。
2 触发条件与前置状态
orgOf 无行 → 400「您尚未加入企业」(四态谱第三态);AssetOwned(id=? AND user_id=? 仅本人)不在 → 404。
3 输入与校验
body asset_id + folder_id *string(指针捕获「缺失/null」vs「空串」——nil 或 "” 最终都落 '' 清除);不校验 folder_id 存在性(双侧同形,悬空 folder_id 可写入)。
4 处理流程
orgOf → AssetOwned → UPDATE asset_library SET folder_id=? WHERE id=? 单写。
5 输出与结果状态
data:true。
6 状态流转
asset_library.folder_id ← folder_id|’’(空串即清除语义,读侧 NULLIF 还原 null)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 未入组织 | 400 | 「您尚未加入企业」 |
| 非本人/不存在素材 | 404 | 「素材不存在」 |
8 权限与数据规则
仅本人素材可归夹(组织共享面读、本人面写的不对称)。
F6.10 成员组(groups 三件)
对应 PRD:§4 M6 · GET/POST /api/organizations/me/groups、DELETE …/groups/:id | 状态:实现 | 实现落点:org_handlers.go:296-337、service.go:475-529、repo_read.go:315-323、repo_groups.go:12-28 | UI:OrganizationView.vue(成员组 tab)| 场景挂钩:OrgChain
1 功能定义
组织成员组列表、创建(leader 可空)、删除(先摘后删)。
2 触发条件与前置状态
GET 无行 → ok([]);POST/DELETE orgOf 无行或非 owner → 各自同哨兵 403(无组织并入 403 非 404);DELETE 无 404 面(PRD ⑧——不查组存在性)。
3 输入与校验
POST body name(trim 必填 400、截 30)+leader_id *string(nil→’’ 落库;不校验 leader 是否本组织成员双侧同形)。
4 处理流程
GET 单读(5 列+leader_id NULLIF+leader_name LEFT JOIN+member_count 子查询);POST mg_ id → 四列单写;DELETE 两写先摘后删:DetachGroupMembers(UPDATE org_members SET group_id='' WHERE group_id=?——按 group_id 全局摘无 org 谓词,冻结 :167 同形)→ DeleteMemberGroup(id=? AND org_id=? org 隔离)。
5 输出与结果状态
MemberGroupRow 行键 id,org_id,name,leader_id,created_at,leader_name,member_count(leader_id/leader_name NULL→null);POST {"id"};DELETE true。
6 状态流转
member_groups 插行/删行;org_members.group_id 批量清空。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非 owner(POST) | 403 | 「仅组织所有者可创建成员组」 |
| name 空 | 400 | 「组名必填」 |
| 非 owner(DELETE) | 403 | 「仅组织所有者可删除」 |
| 删不存在/他组织 id | 200 | 恒 true(恒 ok 死分支 PRD ⑧——403 后无 404 面) |
8 权限与数据规则
ownerOnly 严格双分支;DELETE 无行数校验(PRD ⑧)。
F6.11 权限组 CRUD(perm-groups 四件)
对应 PRD:§4 M6 · GET/POST /api/organizations/me/perm-groups、PUT/DELETE …/perm-groups/:id | 状态:实现 | 实现落点:org_handlers.go:338-401、service.go:532-627+747-775、repo_read.go:333-340、repo_groups.go:31-55 | UI:OrganizationView.vue(权限组 tab)| 场景挂钩:OrgChain(material-perms 联动)
1 功能定义
权限组(四键 perms 白名单 view/edit/delete/download)列表/建/改(COALESCE 部分更新)/删。
2 触发条件与前置状态
GET 无行 → ok([]);POST/PUT/DELETE owner 严格 → 各自 403(文案各异 model.go:26-28);PUT 夹不存在 → 404 英文;DELETE 无 404 面(PRD ⑧)。
3 输入与校验
POST body name(trim 必填截 30)+description(不 trim截 100)+perms []string(FilterPermKeys 白名单保输入序,perms.go:42-53);PUT body 三键全 *string/[]string——hasPerms=req.Perms != nil(perms 缺省/null 沿用旧值;name 传 nil 或空串均沿用 COALESCE 双闸;description 传 "” 为清空)。
4 处理流程
POST pg_ id → 五列单写(perms=JSON 串);PUT PermGroupByID → name=COALESCE(?,name), description=COALESCE(?,description), perms=?(perms 恒写,repo_groups.go:39-43);DELETE 两写先摘后删:DetachPermGroupMembers(perm_group_id=’’)→ DeletePermGroup(org 隔离)。
5 输出与结果状态
GET 行经 permGroupDoc map 重出六键 id,org_id,name,description,perms,created_at——perms 由 json:"-" 串解析重出为数组(空/坏串/nil 一律 [],service.go:763-775);POST {"id","perms"};PUT {"perms"};DELETE true。
6 状态流转
perm_groups 插行/三列部分更新/删行;org_members.perm_group_id 批量清空。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非 owner(三写面各自) | 403 | 「仅组织所有者可创建/修改/删除权限组」(文案各异) |
| name 空(POST) | 400 | 「权限组名称必填」 |
| PUT 组不存在 | 404 | “not found” |
| DELETE 不存在 | 200 | 恒 true(PRD ⑧) |
8 权限与数据规则
ownerOnly 四处;COALESCE 三闸差异(name 空串沿用 vs description 空串清空 vs perms 恒写)——part-update 语义三态勿统一。
F6.12 成员指派(members/:memberId/assign PUT)
对应 PRD:§4 M6 · PUT /api/organizations/me/members/:memberId/assign | 状态:实现 | 实现落点:org_handlers.go:402-419、service.go:645-671、repo.go:184-187 | UI:OrganizationView.vue(成员 tab)| 场景挂钩:OrgChain
1 功能定义
owner 将成员指派到成员组/权限组(双列可空清除)。
2 触发条件与前置状态
orgOf 无行/非 owner → 403;MemberByID 无行 → 404「成员不存在」;不校验 group_id/perm_group_id 存在性(双侧同形,悬空可写)。
3 输入与校验
body group_id/perm_group_id 双 *string(nil→"" 空串语义=清除)。
4 处理流程
判权 → MemberByID → UPDATE org_members SET group_id=?, perm_group_id=? WHERE id=? 单写双列。
5 输出与结果状态
{"group_id","perm_group_id"}——回 "" 而非 null(缺省时冻结回 null,Go 响应面未走读侧 NULLIF——已登记 PRD ⑪,前端 assign 后重拉非回归)。
6 状态流转
org_members.group_id/perm_group_id 双列覆写(传 nil 即清空)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非 owner | 403 | 「仅组织所有者可指派」 |
| target 无行 | 404 | 「成员不存在」 |
8 权限与数据规则
ownerOnly;无 owner-target 豁免判定(可对 owner 行写 group_id——冻结同形)。
F6.13 素材权限矩阵(material-perms 两件)
对应 PRD:§4 M6 · GET /api/organizations/me/material-perms、PUT …/material-perms/:memberId | 状态:实现 | 实现落点:org_handlers.go:421-449、service.go:674-725+686-696、perms.go:7-39、repo.go:190-193 | UI:OrganizationView.vue(素材权限 tab)| 场景挂钩:OrgChain(联动归一断言)
1 功能定义
组织全员素材权限矩阵读、单成员五键权限设置(联动归一幂等)。
2 触发条件与前置状态
GET 无行 → ok([]),无 role 门槛(全员可读矩阵);PUT orgOf 无行/!adminOrOwner → 403(本域唯一 admin 可用面,service.go:50-53=冻结 e.js:240);target 无行 → 404;target 是 owner → 400「所有者默认拥有全部权限」。
3 输入与校验
PUT body perms []string——NormalizeMaterialPerms 内联归一(perms.go:14-39):白名单过滤→upload/download/edit/delete 任一自动补 view→delete 自动补 edit→无 view 则整体清空返 []→按 view,upload,download,edit,delete 序输出。不传(nil)→ 归一空集 []。
4 处理流程
GET MaterialPermRows 单读(COALESCE(material_perms,'[]')+perm_group_id NULLIF,org_members JOIN users 全员行)→ service map 重出;PUT 归一 → 单写 JSON 串。
5 输出与结果状态
GET 行 map 六键 member_id,role,nickname,email,perms,perm_group_id(perms 数组重出);PUT {"perms"}(归一后数组)。无分页(全员行)。
6 状态流转
org_members.material_perms ← JSON 串(如 ["view","edit"])。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非 owner 非 admin | 403 | 「仅组织管理员可设置素材权限」(adminOrOwner 唯一面) |
| target 无行 | 404 | 「成员不存在」 |
| target 是 owner | 400 | 「所有者默认拥有全部权限」 |
8 权限与数据规则
toggleMatPerm 联动 quirk(perms.go:12-13 注:取消 view 即取消全部——归一幂等与冻结逐字同形故 PRD 未单列);owner target 不可设(恒全权)。
F6.14 全站搜索(search)
对应 PRD:§4 M6 · GET /api/search | 状态:实现 | 实现落点:org_handlers.go:451-459、service.go:728-741、repo_read.go:353-371 | UI:App.vue 全局搜索框(:259
/search?q=)| 场景挂钩:OrgChain(share/search 面)
1 功能定义
全局搜索——剧本库(三列 LIKE TOP8)+编剧档案(三列 LIKE TOP6)。
2 触发条件与前置状态
仅登录;无组织判定(任何登录用户搜全站;归 M6 组系冻结 enterprise.js 物理位置)。
3 输入与校验
query q——TrimSpace 后空 → 直接回空双数组不查库。
4 处理流程
SearchScripts(title/description/tags 三列 %q% LIKE,enabled=1,ORDER BY rating DESC LIMIT 8,tier CASE 表达式内联)→ SearchWriters(name/bio/genres 三列 LIKE,LIMIT 6,published 子查询计数)串行两查。
5 输出与结果状态
{scripts,writers};ScriptHit 行键 id,title,genre,market,form,rating,cover_path,description,tier(cover_path/description NULL→null,tier 恒在 ‘精品’|‘普通’);WriterHit 行键 id,user_id,name,bio,genres,works,published。
6 状态流转
纯读零写。
7 边界与异常
无业务异常面;空 q 返 {scripts:[],writers:[]}。
8 权限与数据规则
quirk:% 通配未转义(q 含 %/_ 充当通配符——双侧同形);跨域读 script_library/writer_profiles(repo.go:91-93 注释登记)。
册尾端点覆盖对账(29/29)
对账 backend/cmd/server/routes.go:108-136 亲数 29 条 ↔ 本册 F6.01~F6.14。口径脚注:冻结侧两口径重合无错位——routes.go:104 注「index.js 组织段 9 + enterprise.js 20」,物理分组 :108-116 恰 9 条(=冻结 index.js:513-597 亲数 9)+ :117-136 恰 20 条(=enterprise.js router.* 亲数 20);#8 share 物理路由冻结在 index.js:574 非 director.js(routes.go:106-107 归组注记)。
| # | 端点(routes.go:line) | 节 | # | 端点(routes.go:line) | 节 |
|---|---|---|---|---|---|
| 1 | POST /organizations/applications(:108) | F6.01 | 16 | POST /organizations/me/material/folders(:122) | F6.07 |
| 2 | GET /organizations/me(:109) | F6.01 | 17 | DELETE /organizations/me/material/folders/:id(:123) | F6.07 |
| 3 | PUT /organizations/me/credit-allocation-mode(:110) | F6.01 | 18 | GET /organizations/me/materials(:124) | F6.08 |
| 4 | POST /organizations/me/invites(:111) | F6.02 | 19 | POST /organizations/me/material/assign(:125) | F6.09 |
| 5 | POST /organizations/join(:112) | F6.02 | 20 | GET /organizations/me/groups(:126) | F6.10 |
| 6 | DELETE /organizations/me/members/:memberId(:113) | F6.03 | 21 | POST /organizations/me/groups(:127) | F6.10 |
| 7 | PUT /organizations/me/members/:memberId/role(:114) | F6.03 | 22 | DELETE /organizations/me/groups/:id(:128) | F6.10 |
| 8 | POST /director3d/:id/share(:115,冻结 index.js:574) | F6.04 | 23 | GET /organizations/me/perm-groups(:129) | F6.11 |
| 9 | GET /organizations/shared-projects(:116) | F6.04 | 24 | POST /organizations/me/perm-groups(:130) | F6.11 |
| 10 | GET /organizations/me/dashboard(:117) | F6.05 | 25 | PUT /organizations/me/perm-groups/:id(:131) | F6.11 |
| 11 | GET /showcase(:118) | F6.06 | 26 | DELETE /organizations/me/perm-groups/:id(:132) | F6.11 |
| 12 | POST /showcase(:119) | F6.06 | 27 | PUT /organizations/me/members/:memberId/assign(:133) | F6.12 |
| 13 | DELETE /showcase/:id(:120) | F6.06 | 28 | GET /organizations/me/material-perms(:134) | F6.13 |
| 14 | GET /organizations/me/material/folders(:121) | F6.07 | 29 | PUT /organizations/me/material-perms/:memberId(:135) | F6.13 |
| 15 | GET /api/search(:136) | F6.14 |
差异纪律:PRD §4 M6 ①~⑪+十九续勘正批注(A1 wire 键/A4 谓词还原/C1 script_text 投影)全引用不重立;本册新立 DIF-31-1(members/ShareRow/assets 行 created_at 键序位次三处同族——键集相等序不同,前端按键取值无感,M4 ① 同族先例整批登记)/DIF-31-2(perm-groups/material-perms 响应 map 字母序输出+PUT perms 传非数组时冻结 200 沿用旧值 vs Go bind 400 类型宽容差)入 99-附录 B。
4.11 - FSD M7 · 分销提现
← 总则 | 附录 → | 上游:PRD §4 M7 实现落点:
backend/internal/domain/distribution/(model 181/service 233/repo 214)+cmd/server/distribution_handlers.go(184 行 11 handler)+cmd/server/routes.go:137-151(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:598-638(用户面 7)+:708-737(状态机 4)两段不连续(中间隔 admin 段 :640-:703)仅差异对拍参照。 域内要点:零域铁律——withdrawals/exchanges 全域不触 users.credits/credit_records/通知/SSE(「返佣写入方在 M8 approve 链」model.go:4-8,唯一写入方 M8 F8.03);available 分母无 status 过滤(cancelled/rejected 永不释放额度——:715 note「收益已解锁」冻结谎言照抄);settings 回退双 shape 不可合并(overview 三键 vs 提现面一键);affiliate_threshold 980 硬编码与 M8 不同源勿统一;W_FLOW 严格邻接状态机(终态无键恒 400);admin PUT 不限属主 vs cancel/confirm 属主强制。bind 形态:POST/PUT 三处裸传 return err(echo 默认 400 无统一信封——与 m12「请求体格式错误」两域不同制)。PRD §4 M7 已登记差异 ①~⑩(prd-qmtv-v1.md:180-184)引用不重立。
M7 分销提现
11 端点全量(用户面 9 条 amw + admin 2 条 amw+adminMw 双挂——物理在 /admin/ 前缀但归本域,M12 裁决⑩)。
F7.01 分销总览(overview)
对应 PRD:§4 M7 · GET /api/distribution/overview | 状态:实现 | 实现落点:distribution_handlers.go:28-37、service.go:34-70、repo.go:57-98 | UI:DistributionView.vue(Promise.all 五请求聚合单 query
['dist-all']保「任一失败单 toast」语义)| 场景挂钩:无专项 e2e(E2E-WD-01 待实现);DistributionChain 第七链集成锚
1 功能定义
下级数/累计佣金/在途提现/分销设置/推广码/开通标志/阈值——四查询七键总览。
2 触发条件与前置状态
登录态(amw)+DB 就绪;无行级前置。
3 输入与校验
无 body/query。
4 处理流程
①CountDownlines(referral_relations JOIN users 语义保留勿简化单表 COUNT——孤儿 invitee 面 COUNT 与冻结 all().length 逐位一致,repo.go:57-58)②SumCommissions(COALESCE 0)③PendingWithdrawSum(冻结双 IN 冗余 → Go 等价单 IN 三态 ('pending','reviewing','topay')——paid 被 AND 掉,repo.go:72-78 结果逐位一致)④GetDistSettings(ErrNoRows → SettingsFallback{0.2,0.05,100} 冻结 :602 字面量)⑤GetUserDist(users 两列窄投影 referral_code,affiliate_enabled)。
5 输出与结果状态
OverviewDoc 七键序 downline_count,total_commission,pending_withdraw,settings,referral_code,affiliate_enabled,affiliate_threshold(model.go:160-168)。settings 双 shape:有行=SettingsRow 四键 id,commission_rate,level2_rate,min_withdraw;无行=SettingsFallback 三键 {commission_rate:0.2,level2_rate:0.05,min_withdraw:100} 无 id 键——两 shape 不可合并。referral_code/affiliate_enabled 在 UserDist 为 json:"-" 经 OverviewDoc 再出。
6 状态流转
只读零写。
7 边界与异常
settings 表错误非 ErrNoRows → 500;其余仅 DB 错误透传。零业务 4xx 面。
8 权限与数据规则
数据严格限本人(inviter_id=?/user_id=?);affiliate_threshold 恒 980 硬编码(model.go:158-159/service.go:70)——与 M8 recharge 读 affiliate_threshold_cny 刻意不同源勿统一(PRD M7 ⑤=M8 ⑤ 双登记)。
F7.02 下线与佣金流水(downlines/records)
对应 PRD:§4 M7 · GET /api/distribution/downlines、GET /api/distribution/records | 状态:实现 | 实现落点:distribution_handlers.go:39-60、service.go:73-82、repo.go:103-117 | UI:DistributionView.vue(聚合 query 内)| 场景挂钩:DistributionChain(seed 对拍 40 佣金断言)
1 功能定义
下线列表(含次级下线数 sub_count)+佣金流水(含来源人昵称 from_name)。
2 触发条件与前置状态
登录态;commission_records 空表时 records 为 [](零域 quirk——唯一写入方 M8 approve 链,repo.go:6 注)。
3 输入与校验
无 body/query。
4 处理流程
downlines 单 SELECT(id,nickname,email,created_at+sub_count 相关子查询,无 ORDER BY 无 LIMIT=冻结 :606 同形);records 单 SELECT(c.* 显式 8 列+u.nickname AS from_name 尾插,ORDER BY created_at DESC **LIMIT 100**)。
5 输出与结果状态
downlines 行键 id,nickname,email,created_at,sub_count;records 行键 id,user_id,from_user_id,order_amount,commission,level,status,created_at,from_name(order_amount/commission float64——0004 DECIMAL 后 int64 对小数行恒 scan error→500,二十二期 A1 收口)。LIMIT 分化:域内仅 records 有 LIMIT 100。
6 状态流转
无写。
7 边界与异常
零行业务异常;空列表 JSON [](gorp nil→MakeSlice 与冻结 .all() 逐位一致)。
8 权限与数据规则
本人维度;from_name JOIN 可能因 from_user 无行而少行(INNER JOIN 同构冻结 :609)。
F7.03 提现列表与申请(withdrawals GET/POST)
对应 PRD:§4 M7 · GET、POST /api/distribution/withdrawals | 状态:实现 | 实现落点:distribution_handlers.go:62-93、service.go:85-123+:155-167、repo.go:121-125+:144-166 | UI:DistributionView.vue(wd 提现表单)| 场景挂钩:E2E-WD-01(待实现——主干「余额划转」措辞与差异①相悖,引用时勘正为「佣金台账划转不动 users.credits」);DistributionChain(⑨⑩ 双断言)
1 功能定义
本人提现工单列表+按可提现额度(Σ佣金−Σ提现)申请提现(佣金台账面非余额)。
2 触发条件与前置状态
POST 前置=金额 ≥ min_withdraw 且 ≤ available。
3 输入与校验
POST body credits float64,bind 裸传 return err(handlers:85-87——echo 默认 400 无统一信封);校验序:①credits==0 || credits<min_withdraw → 400 动态文案(!credits 语义=0)②credits>available → 400「可提现积分不足」。
4 处理流程
GET 单 SELECT 7 列 ORDER BY created_at DESC 无 LIMIT。POST:①读 settings 取 min_withdraw——回退仅 {min_withdraw:100} 一键(与 overview 回退三键不同 shape 不共用,PRD ④)②算 available=Σcommission−Σwithdrawals(分母 SUM 无 status 过滤——cancelled/rejected 永不释放额度,:715 note「收益已解锁」冻结谎言照抄,PRD ②)③w_ id → INSERT 三列 (id,user_id,credits),status/created_at 走 DDL DEFAULT(‘pending’)。
5 输出与结果状态
WithdrawalRow 行键序 id,user_id,credits,status,created_at,note,reviewed_at(note/reviewed_at 可空→null 透传);POST {"id":"w_..."}。
6 状态流转
INSERT 即建 pending 态工单(后续流转 F7.05/F7.06)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 金额 0 或 < min_withdraw | 400 | 最低提现 %d 积分(动态 min_withdraw 插值=冻结 :619 模板串) |
| 超出可提现额度 | 400 | 「可提现积分不足」 |
8 权限与数据规则
credits 绑定 float64=冻结 +req.body.credits 强转(PRD ⑥——冻结接受 JSON 数字串 Go bind 拒之);零域:不触 users.credits/credit_records/通知/SSE。
F7.04 兑换(exchanges GET/POST)
对应 PRD:§4 M7 · GET、POST /api/distribution/exchanges | 状态:实现 | 实现落点:distribution_handlers.go:95-124、service.go:128-148、repo.go:128-132+:152-172 | UI:DistributionView.vue(兑换表单)| 场景挂钩:DistributionChain(死态断言)
1 功能定义
佣金积分兑换列表+申请兑换(佣金台账面,min=10 常量不读 settings)。
2 触发条件与前置状态
POST 前置=金额 ≥10 且 ≤(Σ佣金−Σ兑换)。
3 输入与校验
POST body credits float64,bind 裸传 return err;校验序:①credits==0 || credits<10 → 400 ②credits>available → 400。
4 处理流程
GET 单 SELECT 5 列无 LIMIT。POST:Σcommissions → ΣexchangesAll(同构无 status 过滤)→ ex_ id → INSERT 三列,status 走 DDL DEFAULT ‘pending’——全冻结无任何 UPDATE 死态照抄(PRD ⑨)。
5 输出与结果状态
ExchangeRow 行键 id,user_id,credits,status,created_at;POST {"id":"ex_..."}。
6 状态流转
exchanges.status 恒 ‘pending’(死态——集成断言钉死)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 金额 0 或 <10 | 400 | 「最低兑换 10 积分」 |
| 超出可兑换额度 | 400 | 「可兑换积分不足」 |
8 权限与数据规则
amw 本人维度;兑换不扣 withdrawals 面——available 独立再减 Σexchanges(service.go:130-131 注=冻结 :630-631)。
F7.05 撤回与确认到账(cancel/confirm)
对应 PRD:§4 M7 · POST /api/distribution/withdrawals/:id/cancel、POST …/:id/confirm | 状态:实现 | 实现落点:distribution_handlers.go:126-145、service.go:181-201、repo.go:177-207 | UI:DistributionView.vue(工单操作)| 场景挂钩:DistributionChain
1 功能定义
属主自撤(仅 pending/reviewing)+打款后属主确认(仅 paid)——状态机用户面两臂。
2 触发条件与前置状态
cancel 前置 status ∈ {pending,reviewing};confirm 前置 status=‘paid’;均先过属主 404 关(WHERE id=? AND user_id=?)。
3 输入与校验
无 body;路径 :id。
4 处理流程
cancel:属主查行 → 状态谓词 → UPDATE ... SET status='cancelled', note='用户撤回(收益已解锁)'——不写 reviewed_at、不回补余额(released 纯展示,repo.go:196-197 注)→ 响应。confirm:属主查行 → status!=="paid" 谓词 → UPDATE ... SET status='done', note='用户确认到账'(同样不写 reviewed_at)→ ok(true)。
5 输出与结果状态
cancel {"cancelled":true,"released":<float>};confirm data:true。
6 状态流转
pending/reviewing →(cancel)→ cancelled;paid →(confirm)→ done。cancelled/done 均终态(W_FLOW 无键恒 400)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 工单不存在/非本人 | 404 | 「工单不存在」(cancel/confirm/admin PUT 三处共享同文案同哨兵) |
| 非 pending/reviewing 撤回 | 400 | 「当前状态不可撤回(已进入打款流程)」(全角括号照抄) |
| 非 paid 确认 | 400 | 「仅在「已打款」状态下可确认到账」(直角引号照抄) |
8 权限与数据规则
amw+属主强制(与 admin PUT 不限属主形成对照);cancel 不写 reviewed_at;released 纯展示不回补额度(「收益已解锁」谎言见 F7.03)。
F7.06 管理端审核状态机(admin withdrawals PUT/GET)
对应 PRD:§4 M7 · PUT、GET /api/admin/withdrawals(/:id) | 状态:实现 | 实现落点:distribution_handlers.go:147-171、service.go:204-233、model.go:44-71、repo.go:135-140+:186-194+:209-214 | UI:AdminView.vue(提现 tab 消费本域端点——wdSet/wdReject 合并 mutation)| 场景挂钩:无专项 e2e(E2E-WD-01 待实现);DistributionChain(逐态迁移+终态 400+bogus 态三断言)
1 功能定义
admin 严格邻接流转提现工单+全量工单列表(归 M7 域系 M12 裁决⑩——物理在 /admin/ 前缀但域逻辑/册归属均本域)。
2 触发条件与前置状态
amw+adminMw 双挂(全组唯二,其余 9 条仅 amw);PUT 前置=工单存在(不限属主 WHERE id=?)且当前态在 W_FLOW 有键且 next ∈ 邻接表。
3 输入与校验
PUT body {"status":string,"note":string|null}(Note *string;note 空串→nil COALESCE 保留旧值=冻结 :732 req.body.note || null);bind 裸传 return err。GET 无参数(无 status 过滤——与 m08 admin list 双分支不同制)。
4 处理流程
PUT:①WithdrawalByID 不限属主 ②W_FLOW[w.Status] miss→nil 等价空表(=冻结 ||[])③线性扫邻接不合 → StatusError{400, 不允许从「X」流转到「Y」}(zhOr miss 原样回显英文态=冻结 W_ZH[s]||s)④UPDATE ... SET status=?, note=COALESCE(?,note), reviewed_at=?——reviewed_at 每次合法流转覆写,值=newUTCLocalTime() UTC 墙钟(与 auth LocalTime 本地时刻意不同源,PRD ⑦)⑤响应。GET:w.* 显式 7 列+email 尾插,ORDER BY created_at DESC 无 LIMIT。
5 输出与结果状态
PUT {"status":next,"zh":"<W_ZH[next]>"};GET 行键序 id,user_id,credits,status,created_at,note,reviewed_at,email。
6 状态流转
W_FLOW 邻接表全表(model.go:47-52=冻结 index.js:708 字面量直译勿优化):
| 当前态 | 可流转 next | 说明 |
|---|---|---|
| pending | reviewing, rejected | 待审核臂 |
| reviewing | topay, rejected | 审核中臂 |
| topay | paid, rejected | 待打款臂 |
| paid | done | 打款完成臂 |
| done / rejected / cancelled | (无键→nil→恒 400) | 三终态禁出 |
W_ZH:pending=待审核/reviewing=审核中/topay=待打款/paid=已打款/done=已完成/rejected=已驳回/cancelled=已撤回。完整迁移链 pending→reviewing→topay→paid→done,任意臂可旁路 rejected,用户臂 cancelled。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 工单不存在 | 404 | 「工单不存在」(三处共享哨兵) |
| 非邻接流转/终态流转/bogus 态 | 400 | 不允许从「%s」流转到「%s」(严格邻接禁跳转——W_ZH miss 原样回显英文态,PRD ⑧) |
| 非 admin | 403 | “forbidden”(adminMw) |
8 权限与数据规则
quirk:PUT 不限属主 vs cancel/confirm 属主(F7.05 对照);note COALESCE 缺省保留;reviewed_at UTC 每次合法流转覆写(cancel/confirm 不写——不对称)。
册尾端点覆盖对账(11/11)
对账 backend/cmd/server/routes.go:141-151 亲数 11 条 ↔ 本册 F7.01~F7.06。口径脚注:冻结源物理排布两段不连续(用户面 7 条 index.js:598-638 + 状态机 4 条 :708-737,中间隔 admin 段);Go 物理分组单块连续 11 条;M12 组注释「41 物理 − withdrawals 2 条归 M7」(routes.go:268-270)——#10/#11 物理在 /admin/ 前缀但归本域(M12 裁决⑩)。
| # | 端点(routes.go:line) | 节 |
|---|---|---|
| 1 | GET /distribution/overview(:141) | F7.01 |
| 2 | GET /distribution/downlines(:142) | F7.02 |
| 3 | GET /distribution/records(:143) | F7.02 |
| 4 | GET /distribution/withdrawals(:144) | F7.03 |
| 5 | POST /distribution/withdrawals(:145) | F7.03 |
| 6 | GET /distribution/exchanges(:146) | F7.04 |
| 7 | POST /distribution/exchanges(:147) | F7.04 |
| 8 | POST /distribution/withdrawals/:id/cancel(:148) | F7.05 |
| 9 | POST /distribution/withdrawals/:id/confirm(:149) | F7.05 |
| 10 | PUT /admin/withdrawals/:id(:150,amw+adminMw) | F7.06 |
| 11 | GET /admin/withdrawals(:151,amw+adminMw) | F7.06 |
差异纪律:PRD §4 M7 ①~⑩ 全引用不重立(含 DECIMAL 收口⑥/UTC reviewed_at ⑦/W_FLOW ⑧/死态⑨/M12 归属⑩);本册零新立 DIF(19 端点 wire 面键集/键序/文案/状态码/校验序与冻结逐位一致——撰写期双侧对拍判定)。注记:Go 注释冻结行号微偏三处(model.go:27-28 引 :633/:636 实 :632/:633、repo.go:168-171 引 :637 实 :635、service.go:130 引 :628-638 实 628-636)——行为零差勘误候选非 DIF。
4.12 - FSD M8 · 充值
← 总则 | 附录 → | 上游:PRD §4 M8 实现落点:
backend/internal/domain/recharge/(model 118/repo 209/service 381)+cmd/server/recharge_handlers.go(161 行 8 handler)+cmd/server/routes.go:152-166(挂载,up8 := BodyLimit("8mb"):158 proof/pay-qrcode 共用)。事实基准=Go 代码;冻结版qmtv/server/recharge.js(8 条 :28-:120,admin 4 条 router 内写死 /admin/ 前缀)仅差异对拍参照。 域内要点:M8 是 M7 的返佣写入方(commission_records 唯一写入方在 approve 链——M7 零域的另一面);404 文案三态分叉(「订单不存在或已处理」vs proof 面「订单不存在」vs 400「已处理不可重复提交凭证」——model.go:21-32 三哨兵);approve 零事务补偿序七步(MarkPaid→到账→两级返佣→资格联动→通知→SSE——禁事务铁律顺合冻结补偿式一致性);reject 双截断不同源(落库 200/通知 100);orderNo=‘RCH’+毫秒+3 位随机非 uid 勿用 ids 包;recharge_orders.credits刻意不 DECIMAL(0004 七列外——到账恒整数);SSE recharge 仅发本人(M12 无 SSE 的对照面)。bind 形态:裸传 return err(与 m12「请求体格式错误」两域不同制)。PRD §4 M8 已登记差异 ①~⑧(prd-qmtv-v1.md:188)引用不重立。
M8 充值
8 端点全量(用户面 4 条 amw + admin 4 条 amw+adminMw 双挂)。
F8.01 充值配置与下单(config/orders GET+POST)
对应 PRD:§4 M8 · GET /api/recharge/config、GET、POST /api/recharge/orders | 状态:实现 | 实现落点:recharge_handlers.go:40-99、service.go:65-165、repo.go:92-97 | UI:RechargeView.vue(config staleTime 5min+凭证上传 mutate)| 场景挂钩:E2E-RCH-01(待实现,二十七期候选);RechargeChain 第十三链集成锚(33 元单 0.66 佣金)
1 功能定义
汇率/收款码/推广阈值三键配置面+本人订单列表+创建充值订单(金额两位舍入换算积分)。
2 触发条件与前置状态
config/orders 无前置;下单前置=舍入后金额 ∈ [1,100000](双 400)。
3 输入与校验
POST body amount_cny float64,bind 裸传 return err;校验序(service.go:151-158=冻结 :37-39):①amount = Round(amountCny×100)/100(正数域 half-away 与 Math.round 同形)②amount==0 || amount<1 → 400「充值金额至少 1 元」③amount>100000 → 400「单笔金额过大」。
4 处理流程
config:GetSetting(pay_qrcode) 空串/缺行→null + creditRate(默认 10)+ affiliateThreshold(默认 980)组装。orders GET:单 SELECT 9 列 NULLIF(proof_path,''),ORDER BY created_at DESC LIMIT 50。POST:舍入双 400 → orderNo = "RCH"+UnixMilli+3 位随机(非 uid 勿用 ids 包,service.go:149 注)→ credits = int64(Round(amount × creditRate())) → ro_ id → INSERT 5 列,status/created_at 走 DDL DEFAULT。
5 输出与结果状态
config 三键序 credit_per_cny,qrcode,affiliate_threshold(qrcode 空串→null);orders 行键序 id,order_no,credits,amount_cny,status,proof_path,note,created_at,reviewed_at(无 user_id;proof_path NULLIF 还原 null、reviewed_at NULL→null);POST 四键序 id,order_no,credits,amount_cny(amount_cny=舍入后两位小数值)。LIMIT 分化:用户面 50/admin 面 200。
6 状态流转
INSERT 建 pending 订单(proof/approve/reject 流转 F8.02/F8.03)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 金额 <1 元 | 400 | 「充值金额至少 1 元」 |
| 金额 >100000 | 400 | 「单笔金额过大」 |
| settings 值非数字 | — | Go 回退默认 vs 冻结 NaN→JSON null(PRD ④) |
| bind 传字符串数值 | 400 | echo 默认(DIF-31-4 扩域:冻结 +amount_cny 强转放行) |
8 权限与数据规则
orders 严格本人 user_id=?;config 全登录用户同值;threshold 980 与 M7 硬编码 980 两处刻意不同源勿统一(PRD ⑤=M7 ⑤);recharge_orders.credits BIGINT 恒整数(0004 刻意不动——到账恒整数)。
F8.02 凭证上传与收款码(proof/pay-qrcode)
对应 PRD:§4 M8 · POST /api/recharge/orders/:id/proof、POST /api/admin/pay-qrcode | 状态:实现 | 实现落点:recharge_handlers.go:18-38/:140-161、service.go:170-230+:364-372、repo.go:100-121 | UI:RechargeView.vue(凭证上传 mutate)/AdminView.vue(收款码设置)| 场景挂钩:RechargeChain(真落盘断言 :114-117)
1 功能定义
订单转账凭证图片上传(属主)+admin 收款二维码设置(落 system_settings KV)——共用同一 upload 函数+8MB 双保险。
2 触发条件与前置状态
proof 前置=订单存在且属主且 status=‘pending’(校验顺序 404→400 非 pending→400 无文件,service.go:170-183=冻结 :50→:51→:52);pay-qrcode 无前置。
3 输入与校验
multipart 单文件字段 file;8MB 双保险:路由级 up8+handler 单文件 8<<20,超限 500「File too large」 模拟 multer LIMIT_FILE_SIZE(handlers:23-26,M9 ④/M12 同制先例)。
4 处理流程
proof:GetOrderOfUser(id=? AND user_id=?)→ 非 pending 400 → 无文件 400 → upload(ObjectKey up_+extOf 默认 .png + store.Put + PathOf /files/+key)→ UPDATE recharge_orders SET proof_path=? 单列(不改 status)。pay-qrcode:无文件 400 → 同一 upload → SetSetting upsert(key 保留字反引号)。
5 输出与结果状态
proof {"proof":"<path>"};pay-qrcode {"qrcode":"<path>"};落库/响应形态 /files/...。
6 状态流转
recharge_orders.proof_path 单列覆写(status 不动);system_settings pay_qrcode upsert 不触订单。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 订单不存在/非本人 | 404 | 「订单不存在」(三态分叉之一——与 approve/reject 的「订单不存在或已处理」不同源) |
| 非 pending 再传凭证 | 400 | 「订单已处理,不可重复提交凭证」(approve 后亦然) |
| 无文件(proof) | 400 | 「缺少凭证图片」 |
| 无文件(pay-qrcode) | 400 | 「缺少二维码图片」 |
| 超 8MB | 500 | 「File too large」 |
8 权限与数据规则
proof=amw+属主;pay-qrcode=amw+adminMw;proof_path Go DDL NOT NULL DEFAULT ’’ vs 冻结可空——三处读侧 NULLIF 还原 null 透传(PRD ⑦)。
F8.03 审核与两级返佣(approve/reject/admin list)
对应 PRD:§4 M8 · POST /api/admin/recharge-orders/:id/approve、POST …/:id/reject、GET /api/admin/recharge-orders | 状态:实现 | 实现落点:recharge_handlers.go:100-139、service.go:234-361、repo.go:125-146+:151-162+:168-200 | UI:AdminView.vue(充值 tab approveRecharge——invalidate 缺口保留不刷 users·summary 照抄)| 场景挂钩:RechargeChain(approve 全链/幂等 404/33 元 0.66 佣金/资格联动四锚)
1 功能定义
admin 审核链——approve 到账+两级返佣+资格联动+通知+SSE(全站返佣唯一写入方);reject 驳回+通知;admin 全量订单列表。approveOrder 冻结导出「供重启复用」系死代码零消费方——Go 不建恢复链(PRD ⑥)。
2 触发条件与前置状态
approve/reject 共享 pending 谓词 WHERE id=? AND status='pending';非 pending(含二跑幂等)→ 404「订单不存在或已处理」(ErrOrderProcessed 两路同源共享哨兵——与 proof 面「订单不存在」不同源,三态分叉之二/一)。
3 输入与校验
approve 无 body;reject body note string(缺省→""),bind 裸传 return err;admin list query status。adminNickname:JWT 零查库经 auth.Me 取行,空 → 兜底 'admin'(=冻结 admin.nickname||'admin')。
4 处理流程(approve 零事务补偿序七步,service.go:236-298=冻结 :59-92 逐条直译)
①MarkPaid:status='paid', reviewed_by=?, reviewed_at=?(reviewed_at=newUTCLocalTime UTC,PRD ②)→ ②到账 addTolerant「付费充值 %s(%s 元)」(仅吞 ErrNoUser 对齐冻结 addCredits 静默,其余 DB 错误照传播——PRD ③)→ ③GetBuyer(affiliate_enabled 单列)→ ④两级返佣:GetReferrer → rel1 → pay(一级) → GetReferrer(rel1) → rel2 → pay(二级);pay:commission=Round(amount×rate×100)/100、<=0 跳过、INSERT commission_records 7 列 status='settled'(0004 后 order_amount/commission 原值入库——差异①终结)、addTolerant 入账「分销收益(一级|二级):下级充值 X 元,佣金 Y」+通知「分销收益到账」(from_user_id 恒=充值人非链上级,PRD ⑧)→ ⑤资格联动:!buyer.AffiliateEnabled && AmountCny>=threshold → EnableAffiliate + 通知「推广资格已开通」(已开通不重复)→ ⑥通知「充值到账」订单 %s 审核通过,%d 积分已到账。 → ⑦SSE hub.Emit(o.UserID,"recharge",{order_no,credits}) 仅发本人。reject(service.go:340-361):pending 404 同源 → notifyNote 缺省「凭证有误」→ MarkRejected 4 值(落库 note=truncateRune(note,200) 空串原样)→ 通知「充值未通过」订单 %s 未通过审核:%s(truncateRune(notifyNote,100)——双截断不同源)→ ok(true)。admin list:status!="" && status!="全部" 双 SQL 分支照抄勿合并(带 WHERE vs 不过滤,等价性勿优化)。
5 输出与结果状态
approve {"order_no","credits"};reject true;admin list 行键序 13 键 id,order_no,user_id,credits,amount_cny,status,proof_path,note,reviewed_by,reviewed_at,created_at,email,nickname(AdminOrderRow 11+2 尾插=冻结 r.* SQLite 列序;version 私货列不入投影)。LIMIT 200。
6 状态流转
pending →(approve)→ paid / →(reject)→ rejected;proof 不改 status;二级状态无出边(GetPendingOrder 谓词使 approve/reject 二跑恒 404——幂等性缺失)。写序=①~⑦ 顺序(MarkPaid 先行——到账/返佣失败时已 paid 的补偿式语义=冻结同构)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非 pending(approve/reject,含二跑) | 404 | 「订单不存在或已处理」(三态分叉之二) |
| 非 pending(proof 面) | 400 | 「订单已处理,不可重复提交凭证」 |
| buyer 不存在窗口 | 200 | addTolerant 吞 ErrNoUser(PRD ③——对齐冻结静默) |
| 佣金计算 ≤0 | — | 静默跳过该级返佣(=冻结 :70) |
8 权限与数据规则
三端点均 amw+adminMw;adminNickname 空兜底 ‘admin’;reviewed_at UTC 刻意与 auth LocalTime 不同源(PRD ②,M7 ⑦ 同制);33 元单集成锚:二级佣金 0.66 原值入库+余额 base+2.66 DECIMAL 往返(integration_recharge_test.go:162-190)。
册尾端点覆盖对账(8/8)
对账 backend/cmd/server/routes.go:159-166 亲数 8 条 ↔ 本册 F8.01~F8.03。口径脚注:冻结 recharge.js 8 条(:28/:32/:36/:48 用户 4+:95/:99/:108/:115 admin 4——admin 靠 router 内写死 /admin/ 前缀,挂载点 index.js:27 app.use('/api', recharge.router));Go 拍平进同一 authed 组、admin 4 条挂 adminMw(routes.go:154-155 注)——冻结-Go 计数自洽。
| # | 端点(routes.go:line) | 节 |
|---|---|---|
| 1 | GET /recharge/config(:159) | F8.01 |
| 2 | GET /recharge/orders(:160) | F8.01 |
| 3 | POST /recharge/orders(:161) | F8.01 |
| 4 | POST /recharge/orders/:id/proof(:162,+up8) | F8.02 |
| 5 | POST /admin/recharge-orders/:id/approve(:163,amw+adminMw) | F8.03 |
| 6 | POST /admin/recharge-orders/:id/reject(:164,amw+adminMw) | F8.03 |
| 7 | GET /admin/recharge-orders(:165,amw+adminMw) | F8.03 |
| 8 | POST /admin/pay-qrcode(:166,amw+adminMw+up8) | F8.02 |
差异纪律:PRD §4 M8 ①~⑧ 全引用不重立(含 DECIMAL 收口①/UTC②/ErrNoUser 容错③/settings NaN④/threshold 双源⑤/死代码⑥/proof_path NULLIF⑦/from_user_id ⑧);本册零独立新立 DIF——下单 bind 强转(冻结 +amount_cny 宽收数值字符串 vs Go bind 400)并入 DIF-31-4 扩域(M7 ⑥ credits 面同族、m12 F12.02/04 同型)。注记:资格联动谓词排序冻结「金额判定在前」vs Go「buyer 判定在外层」——纯谓词重排真值表同构无副作用差异,不立目。
4.13 - FSD M9 · 剧本库与投稿
← 总则 | 附录 → | 上游:PRD §4 M9 实现落点:
backend/internal/domain/scriptlib/(model/repo/service〔scriptlib.js 9+ensureSeed〕/service_studio〔studio.js 16〕/seeds)+cmd/server/scriptlib_handlers.go+cmd/server/routes.go:167-202(挂载)。事实基准=Go 代码;冻结版qmtv/server/scriptlib.js/studio.js仅差异对拍参照。 域内要点:admin 9 端点叠AdminMiddleware(403 forbidden);wire 全蛇形(model.go json tag 亲证零驼峰);两套正则刻意勿合并(seedEpisodeRe 带\s*service.go:78 vs catalogRe 无:196);两套Genres 清单(scriptlib.js 11 项 service.go:25 vs studio.js studioGenres 11 项 service_studio.go:21——冻结死导出未消费,Go 不移植);ID 前缀 sl_/su_/sub_/wp_/up_/na_(ids.go:29-50)。
M9 剧本库与投稿
25 端点全量(scriptlib.js 9:用户 4+admin 5;studio.js 16:market 2/cover·rating 2/submissions 4/writer-profile 2/writers 2/admin·submissions 2/narrations 2)。公开写面唯一扣费点=解说成片 30 积分;审核 publish 多表写禁事务(置位→自动建档→入库→通知逐条)。PRD §4 M9 已登记差异 ①~⑨(prd-qmtv-v1.md:196)引用不重立。
F9.01 剧本库目录与检索(genres / list / :id / use)
对应 PRD:§4 M9 · GET /api/script-library/genres、GET /api/script-library、GET /api/script-library/:id、POST /api/script-library/:id/use | 状态:实现 | 实现落点:scriptlib_handlers.go:31-80、service.go:157-280、repo.go:285-391 | UI:ScriptLibraryView + DashboardView 剧本库弹窗 | 场景挂钩:无专门 e2e(E2E-SUB-01 间接)
1 功能定义
平台剧本库只读消费面:五维目录(genres/markets/audiences/forms/stats)、四维筛选列表(含 7 日热度排序)、详情(试读+故事目录+使用计数)、「使用」埋点。
2 触发条件与前置状态
- 四端点均登录态(amw);genres/list 首触惰性种子
ensureSeed(service.go:92-142:空表整插 3 条种子剧;否则按标题找 enabled=1 行仅回填占位值——market「中国内地」/""、audience「大众」/""、form「短剧」/"",service.go:70-74;content 无「第X集」标题整替为新分集版,seedEpisodeRe service.go:78)。 - 详情/使用前置:script 存在且
enabled=1(repo.go:347-357),否则 404「剧本不存在或已下架」。
3 输入与校验
- list query 五参:genre/market/audience/form/search/sort——四维「全部」或空=忽略该维(repo.go:307-323);search TrimSpace 后 LIKE title/description/tags 三字段(repo.go:324-328);sort 三态
rating/new/缺省 hot(repo.go:331-337)。 - detail/use 路径参数 :id 无格式校验(查无→404 哨兵)。
4 处理流程
- genres:ensureSeed → GROUP BY genre 产 stats(scripts/uses_7d/uses_total,repo.go:285-296,ORDER BY uses_7d DESC)→ 五键 payload 固定序 genres/markets/audiences/forms/stats(model.go:124-130;常量 11/3/4/3 项 service.go:25-28)。
- detail:preview=
preview_text||content 前 800 字(service.go:248-251)→ char_count=rune 数(service.go:262,PRD ⑥)→ catalog 目录切分(第X集lookahead 手动切——RE2 不支持,service.go:202-238;filter(Boolean) 后 >1 段才产出〔前言段也算一段照抄〕;title=首行 ≤40 字,空回退「第 N 集」)→ uses_total/uses_7d 两计数。 - use:存在校验 → INSERT script_uses(su_ 前缀,service.go:276)→ 响应恒
true(不回显行)。
5 输出与结果状态
- 列表行=
s.*+char_count/uses_total/uses_7d 三别名尾插(model.go:90-95,冻结 scriptlib.js:152 STATS_SELECT 键序);详情=行+char_count/preview/catalog/uses_total/uses_7d(catalog 在 uses 前,model.go:105-112)。 - genres stats 行 {genre, scripts, uses_7d, uses_total}(model.go:116-121)。
6 状态流转
script_library.enabled 只被 admin 面(F9.05 PUT enabled)翻转;本组四端点零写状态(use 仅追加 script_uses 流水)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 详情/使用 id 不存在或已下架 | 404 | 「剧本不存在或已下架」(model.go:201) |
| uses_7d 时间窗 | created_at >= NOW() - INTERVAL 7 DAY(repo.go:377,MySQL 方言) | — |
| 空表 stats | GROUP BY 零行→空数组(service.go:168-170 nil→[],DIF-29-1 同族形态) | — |
| content 无分集标题 | catalog 空数组(≤1 段,service.go:226-228) | — |
8 权限与数据规则
四端点 authed + amw;script_uses 不去重(同一用户重复 use 多计——冻结同形照抄);列表默认排 hot=uses_7d DESC, uses_total DESC, rating DESC(repo.go:331)。
F9.02 剧本市集(market/scripts 二件)
对应 PRD:§4 M9 · GET /api/market/scripts、GET /api/market/scripts/:id | 状态:实现 | 实现落点:scriptlib_handlers.go:214-237、service_studio.go:31-46、repo.go:421-468 | UI:ScriptLibraryView 市集 tab | 场景挂钩:无
1 功能定义
已上架剧本的市集浏览面:与 F9.01 同构的列表/详情,但独立筛选维(无 audience)与不同 404 文案。
2 触发条件与前置状态
登录态;列表无 ensureSeed(与 scriptlib 面刻意不同——种子只保证库面有货);详情前置 enabled=1。
3 输入与校验
query 四参 genre/market/form/search/sort——「全部」忽略同 F9.01;search 直用原值不 TrimSpace(repo.go:439-440 注释「两处刻意不同源勿统一」——与 F9.04 writers trim 对比,PRD ⑧)。
4 处理流程
列表 SELECT 与 F9.01 同 statsSelect 投影但独立 WHERE(market 面固定 enabled=1);详情 GetMarketScriptEnabled(repo.go:457-468)。
5 输出与结果状态
列表行同 ScriptListRow;详情=ScriptLibrary 裸行(无 preview/catalog/uses 派生键——与 F9.01 详情不同构,model.go:11-34)。
6 状态流转
零写面(纯读)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 详情不存在/下架 | 404 | 「剧本不存在」(model.go:203——与 F9.01「剧本不存在或已下架」不同字勿统一) |
8 权限与数据规则
authed + amw;market 面不展示 disabled 行;sort 三态同 F9.01。
F9.03 投稿工作流(submissions 四件)
对应 PRD:§4 M9 · GET/POST /api/studio/submissions、PUT /api/studio/submissions/:id、POST /api/studio/submissions/:id/submit | 状态:实现 | 实现落点:scriptlib_handlers.go:271-329、service_studio.go:102-223、repo.go:532-593 | UI:StudioView 投稿区 | 场景挂钩:E2E-SUB-01(投稿链,待实现——人工步骤即未来断言主干)
1 功能定义
编剧投稿全生命周期自助面:建稿(存草稿或直提)、编辑、提交送审、我的稿件列表。
2 触发条件与前置状态
登录态(amw);编辑/提交前置:行存在且属主(WHERE id=? AND user_id=?,repo.go:557-568),否则 404「not found」。
3 输入与校验
- create:title/content 必填 400「标题与内容必填」(service_studio.go:131-133);截断 title 80/genre 12/market 20/form 12/audience 60/description 300(service_studio.go:137-142);genre/market/form 缺省「都市」/「中国内地」/「短剧」。
- edit:全字段指针可选(nil=不动,COALESCE 形态 service_studio.go:182-203);
Submit bool直传。
4 处理流程
- create:status=
submit?'pending':'draft'(service_studio.go:145-149)→ INSERT sub_ 行 → 响应{id, status}。 - edit:状态守卫(仅 draft/needs_change/rejected 可编辑,:175-177)→ status=
submit?'pending':原值→ COALESCE 更新 → 恒 true。 - submit:仅 draft 可提交(:216-218)→ 置 pending(note 不动)→ 恒 true。
5 输出与结果状态
列表=本人窄投影 10 列不含 content(model.go:133-144,冻结 studio.js:171 列序);create 响应 {id, status}(handlers :298)。
6 状态流转
flowchart LR
A["draft"] -->|"edit(submit)/submit"| B["pending"]
B -->|"admin review"| C["needs_change"]
B -->|"admin review"| D["rejected"]
B -->|"admin publish"| E["published"]
C -->|"edit/submit"| B
D -->|"edit/submit"| Bneeds_change/rejected 可再编辑回 pending;published/pending 不可编辑(400)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 编辑/提交非本人或不存在的 :id | 404 | 「not found」(model.go:205) |
| 编辑非 draft/needs_change/rejected | 400 | 「当前状态不可编辑」(model.go:218) |
| submit 非 draft | 400 | 「该稿件已提交过」(model.go:220) |
| 越权探测 | 属主过滤 404 不暴露存在性 | — |
8 权限与数据规则
四端点 authed + amw 无 admin;全部 user_id 属主过滤;content 全文仅 create/PUT 入库,列表不下发。
F9.04 编剧档案与编剧名录(writer-profile 二件 + writers 二件)
对应 PRD:§4 M9 · GET/POST /api/studio/writer-profile、GET /api/writers、GET /api/writers/:id | 状态:实现 | 实现落点:scriptlib_handlers.go:333-385、service_studio.go:227-320、repo.go:622-749 | UI:StudioView 档案区 + WritersView | 场景挂钩:无
1 功能定义
编剧自助档案(一人一份 user_id UNIQUE)+ 公开编剧名录(列表/详情带 published 计数)。
2 触发条件与前置状态
profile GET 无前置(查无=null);save 笔名必填;writers/:id 前置档案存在,否则 404「编剧不存在」。
3 输入与校验
save:name 必填 400「笔名必填」(service_studio.go:258-260);截断 name 30/bio 500/genres 120/works 500(:261-264)。
4 处理流程
- GET:{profile, published} 双读(published=本人 script_library published 计数,repo.go:669-680);profile 查无 null 非 404(service_studio.go:233)。
- save:exist→UPDATE 返回原 id;否则 INSERT wp_(:265-282)。
- writers 列表:6 列+published 子查询尾插(model.go:153-160);search TrimSpace 后 LIKE(PRD ⑧ 用户裁决保持)。
- 详情:{…row, scripts, joined_at}(model.go:175-179);scripts=该编剧 published LIMIT 30(repo.go:727-737);joined_at=users.created_at 别名。
5 输出与结果状态
save 响应 {id};writers 列表行/详情键序照冻结 studio.js:98-105(列表 SELECT)/:111-119(详情 {...w, scripts, joined_at})。
6 状态流转
writer_profiles 无状态列;published 计数随 admin review(F9.07)联动。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 编剧详情 :id 不存在 | 404 | 「编剧不存在」(model.go:207) |
| joined_at 底层 users 行查无 | GetWriterJoinedAt 原生 QueryRow 透传错误→500(repo.go:713;冻结 null 兜底——FK 语义死路径,DIF-30-1 登记) | — |
8 权限与数据规则
四端点 authed + amw(writers 名录非公开面);档案一人一份(UNIQUE upsert 语义);越权不可改他人档案(按 token user_id 定位)。
F9.05 管理面剧本库维护(admin script-library CRUD 四件)
对应 PRD:§4 M9 · GET/POST /api/admin/script-library、PUT/DELETE /api/admin/script-library/:id | 状态:实现 | 实现落点:scriptlib_handlers.go:84-210、service.go:284-492、repo.go:298/:393-420/:516-531 | UI:AdminView 剧本库 tab | 场景挂钩:无
1 功能定义
平台剧本库管理面:全量列表(含 disabled)、建库、21 列可选更新、删除。
2 触发条件与前置状态
adminMw(403 forbidden);list 首触 ensureSeed 同 F9.01;PUT/DELETE 不校验存在性——id 不存在也恒 true(repo.go:397 注释「id 不存在也恒 ok」照抄冻结)。
3 输入与校验
- create:title/content 必填 400「标题与剧本内容必填」(service.go:363-365);截断 title 80/genre 12 缺省「其他」/description 300;ADMIN_FIELDS 11 扩展字段——TEXTUAL 6 项(highlights/preview_text/characters/worldview/synopsis/setting)截 4000、其余截 100(extraOf service.go:321-344,create 分叉 maxTextual=4000);episodes/planned_chars 负数归 0(nonNeg service.go:516-521);rating clamp 0..5(service.go:542-550)。
- put:同构全字段指针可选;COALESCE 21 列(repo.go:398-409);不传字段不动。
4 处理流程
create:19 列 INSERT(6 TEXT 列落字符串空串非 NULL——冻结显式绑定形态,service.go:144-155 注)→ 响应 {id}。delete:先删 uses 再删主行(两删序固定 service.go:484-492,禁事务铁律同形)。
5 输出与结果状态
list=ScriptListRow 同构但 AdminAll 无 enabled 过滤且固定 created_at DESC(repo.go:338-340);put/delete 恒 true。
6 状态流转
enabled 由 PUT 翻转(F9.01/F9.02 消费面即时可见);无其他状态。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| create 缺 title/content | 400 | 「标题与剧本内容必填」(model.go:210) |
| PUT/DELETE id 不存在 | 200 恒 true(0 行 UPDATE/先删 uses 0 行)——无 404 面(quirk 照抄冻结) | — |
| 非管理员访问 | 403 | 「forbidden」 |
8 权限与数据规则
四端点 authed + adminMw;删除级联仅两表(uses→主行;无 submissions 关联);bind 失败静默按空载荷(_ = c.Bind handlers :119/:189——DIF-30-2 登记,DIF-29-23 同族)。
F9.06 批量上传与封面评分(upload + cover + rating)
对应 PRD:§4 M9 · POST /api/admin/script-library/upload、POST /api/admin/script-library/:id/cover、PUT /api/admin/script-library/:id/rating | 状态:实现 | 实现落点:scriptlib_handlers.go:132-182/:239-267、service.go:411-449、service_studio.go:48-98、repo.go:469-484 | UI:AdminView 剧本库 tab(上传/封面/评分)| 场景挂钩:无
1 功能定义
管理面批量导入 txt 剧本(GBK 自动回退)、单封面图上传、评分设定。
2 触发条件与前置状态
adminMw;upload 前置 multipart files 字段非空;cover 前置文件非空;upload 修后语义(PRD ①):冻结 :248 run 16 参数对 9 占位符恒 RangeError——冻结版 upload 从未可用,Go 按 9 列 INSERT 修后实现(service.go:412-417 注)。
3 输入与校验
- upload:单文件 ≤2MB(超→500「File too large」handlers :149-151 multer LIMIT_FILE_SIZE 语义)、≤20 文件(超→500「Unexpected field」:144-146);路由级 BodyLimit 45mb 双保险(PRD ④);文件名过
gateway.FixFilename(:162,GBK 坑守卫);extra 统一截 100 不走 TEXTUAL 4000(service.go:426,与 create 分叉)。 - 内容判定:UTF-8 解码统计 U+FFFD 乱码率 > max(2, 长度×0.002) 回退 GB18030 解码(decodeUploadContent service.go:591-600,PRD ③);TrimSpace 后 <30 字跳过(:430-432)。
- cover:缺文件 400「缺少封面图」(service_studio.go:52-54);扩展名缺省 .png(extOf :70-75)。
- rating:clamp 0..5(service_studio.go:92-98);nil rating 传 0(clampRating nil→0)。
4 处理流程
- upload:逐文件解码→过滤→INSERT(title=
prefix+stripExt(文件名)截 80,genre 缺省「其他」截 12,service.go:432-443)→{created:[{id,title}], count};全跳过→400「没有有效剧本(文件过短或非文本)」(:445-447)。 - cover:storage.Put(up_ 前缀 ObjectKey 日期目录)→ cover_path 落库
PathOf(key)(F8 口径绝对 URL,PRD ②;冻结 /files/ 相对)→ 响应{cover: path}。 - rating:UPDATE rating 单列 → 恒 true。
5 输出与结果状态
upload {created, count};cover {cover};rating true。
6 状态流转
新库行 enabled=1 直接上架(upload 语义);cover/rating 零状态。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| upload 无 files 字段 | 400 | 「未选择文件」(handlers :141-143) |
| 单文件 >2MB / >20 文件 | 500(multer 语义对齐,PRD ④) | 「File too large」/「Unexpected field」 |
| 全部文件 <30 字或非文本 | 400 | 「没有有效剧本(文件过短或非文本)」(model.go:214) |
| cover 缺文件 | 400 | 「缺少封面图」(model.go:224) |
| storage 未配置(降级模式) | NoopStore.Put 报错 | 500(ErrNotConfigured 在 errRespond 零映射落 default;503 仅 requireDB 前置可达——storage.New 恒装配 NoopStore) |
8 权限与数据规则
三端点 authed + adminMw;upload 每文件独立 su_ 无预生成(9 列 INSERT 不产 uses);cover ObjectKey=up_ 前缀+原始扩展名。
F9.07 投稿审核与自动上架(admin/submissions 二件)
对应 PRD:§4 M9 · GET /api/admin/submissions、POST /api/admin/submissions/:id/review 🔒 | 状态:实现 | 实现落点:scriptlib_handlers.go:389-414、service_studio.go:325-417、repo.go:594-621/:738-759 | UI:AdminView 投稿审核 tab | 场景挂钩:E2E-SUB-01(publish 分支,待实现)
1 功能定义
投稿审核台:按状态筛选全量列表 + 三动作审核(needs_change/reject/publish),publish 附带自动上架与建档。
2 触发条件与前置状态
adminMw;review 前置:投稿行存在(不限属主,repo.go:611-621),否则 404「not found」;action 三值白名单。
3 输入与校验
GET query status(「全部」或空=全量,repo.go:594-610);review body {action, note}——action ∉ {needs_change, reject, publish} → 400「未知操作」(service_studio.go:360-364);note 截 200(:359)。
4 处理流程(publish 四步禁事务)
- 置状态+note(映射 needs_change→needs_change/reject→rejected/publish→published,:371;note 空回默认文案
reviewNoteDefault——publish 默认「已发布」,:344-349)。 - publish 且无档案→自动建档 wp_(nickname 空回退「编剧」+user_id 前 4,:385-387)。
- publish 入库 script_library(sl_ 前缀、preview_text=content 前 400,:392-405)。
- 通知「投稿状态更新」:《title》:reviewNotice 文案(publish=「已发布到剧本库」——与 UPDATE note 默认「已发布」不同字勿统一,:336-342);括号注记用原始 note 判空非默认值(:365-366 注)。
5 输出与结果状态
list=sub.*+u.email 尾插(model.go:147-150);review 恒 true。
6 状态流转
与 F9.03 §6 状态机同一张表(admin 侧三出边);publish 终态+库行落地上架。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| :id 不存在 | 404 | 「not found」 |
| action 非三值 | 400 | 「未知操作」(model.go:222) |
| publish 四步中途失败 | 无事务回滚——半态留存(禁事务铁律=冻结无事务同形) | — |
8 权限与数据规则
authed + adminMw;review 不校验投稿当前状态(pending/rejected 均可再 publish——无状态机出边限制,quirk 照抄冻结);通知零 SSE(M5 读面 60s 轮询消费)。
F9.08 解说一键成片(tools/narrations 二件)
对应 PRD:§4 M9 · POST /api/tools/narrations、GET /api/tools/narrations | 状态:实现 | 实现落点:scriptlib_handlers.go:418-443、service_studio.go:421-565、repo.go:760-784 | UI:IndependentToolsView 解说工具 | 场景挂钩:无
1 功能定义
剧本解说词一键成片:同步 Mock 出 SVG 占位视频+解说分段 JSON,单次扣 30 积分。
2 触发条件与前置状态
登录态;script_text TrimSpace 后 <50 字 → 400「剧本文本过短(至少 50 字)」(service_studio.go:450-452);余额 <30 → 402。
3 输入与校验
{title?, script_text};title 空→「解说成片」落库截 60(:457-458/:478);script_text 落库截 100000 rune(:469)。
4 处理流程
扣费先行(TryDeduct 30 type='video'「解说剧一键成片(-30)」taskID=NULL——0003 后 ''→NULL 绑定收口,:453)→ 空行切段(/\n\s*\n/ 等价贪婪切,splitOnBlankLines :527-555)≤30 段 → 每段取首个含「:」或「:」的行 ≤80 字(无则整段,firstColonLine :558-565)→ visual 按 i 奇偶交替(偶「主体中景推进」/奇「城市夜景空镜」:512-515)→ MakeVideo SVG 占位落盘(mgv_ 族 key)→ result JSON 固定键序 segments/cost/engine(:425-429;engine=管理台 video 引擎名)→ INSERT na_ 行 status=‘done’(同步完成无轮询)。
5 输出与结果状态
create 响应 {id, url, segments, cost}(url=落盘 PathOf,降级模式空串);list=本人 6 列窄投影(id/title/status/file_path/result/created_at,model.go:182-189);result 原样 JSON 字符串下发。
6 状态流转
status 恒 ‘done’(同步 Mock 无 processing/failed——Mock 行为是契约的一部分,总则 §2.4)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| <50 字 | 400(扣费前校验) | 「剧本文本过短(至少 50 字)」(model.go:228) |
| 余额不足 | 402 | credits.ErrInsufficient 统一信封 |
| storage 未配置 | NoopStore.Put 报错→500,扣 30 不退、任务不落(service_studio.go:462-468 nil 守卫恒不触发——storage.New 恒装配 NoopStore,非「跳过照常」;参照 m13 同位置死路径表述) | — |
8 权限与数据规则
authed + amw;扣费台账 type=‘video’ 与视频生成同族(credit-records 聚合归视频面);PRD ⑨ task_id 已随 0003 NULL 化收口。
册尾端点覆盖对账(25/25)
对账 backend/cmd/server/routes.go:167-202 亲数 25 条 ↔ 本册 F9.01~F9.08:
| # | 端点(routes.go:line) | 节 | # | 端点(routes.go:line) | 节 |
|---|---|---|---|---|---|
| 1 | GET /script-library/genres(:176) | F9.01 | 14 | GET /studio/submissions(:191) | F9.03 |
| 2 | GET /script-library(:177) | F9.01 | 15 | POST /studio/submissions(:192) | F9.03 |
| 3 | GET /script-library/:id(:178) | F9.01 | 16 | PUT /studio/submissions/:id(:193) | F9.03 |
| 4 | POST /script-library/:id/use(:179) | F9.01 | 17 | POST /studio/submissions/:id/submit(:194) | F9.03 |
| 5 | GET /admin/script-library(:180)🔒 | F9.05 | 18 | GET /studio/writer-profile(:195) | F9.04 |
| 6 | POST /admin/script-library(:181)🔒 | F9.05 | 19 | POST /studio/writer-profile(:196) | F9.04 |
| 7 | POST /admin/script-library/upload(:182)🔒 | F9.06 | 20 | GET /writers(:197) | F9.04 |
| 8 | PUT /admin/script-library/:id(:183)🔒 | F9.05 | 21 | GET /writers/:id(:198) | F9.04 |
| 9 | DELETE /admin/script-library/:id(:184)🔒 | F9.05 | 22 | GET /admin/submissions(:199)🔒 | F9.07 |
| 10 | GET /market/scripts(:187) | F9.02 | 23 | POST /admin/submissions/:id/review(:200)🔒 | F9.07 |
| 11 | GET /market/scripts/:id(:188) | F9.02 | 24 | POST /tools/narrations(:201) | F9.08 |
| 12 | POST /admin/script-library/:id/cover(:189)🔒 | F9.06 | 25 | GET /tools/narrations(:202) | F9.08 |
| 13 | PUT /admin/script-library/:id/rating(:190)🔒 | F9.06 |
注:🔒=adminMw;admin 9 端点(#5-9/12-13/22-23)。PRD §4 M9 差异 ①~⑨ 全引用不重立;本册新立 DIF-30-1(joined_at 500 vs 冻结 null 死路径)/ DIF-30-2(bind 静默按空载荷,DIF-29-23 同族)入 99-附录 B。
4.14 - FSD 附录 · PRD 对照 / 代码域索引 / 差异台账
附录 A1:PRD 模块 → FSD 分册对照(权威)
| PRD 模块 | 分册 | F-ID 段 | 端点数 | 覆盖期 |
|---|---|---|---|---|
| 基础(SSE/health/release-mode) | 总则 §2.2/§2.5 覆盖,无独立分册 | — | 3 | 骨架期 |
| M1 认证与用户 | m01-认证与用户 | F1.01~ | 9 | 二十八期 |
| M2 任务与拆解 | m02-任务与拆解 | F2.01~ | 38 | 二十九期 |
| M3 生成与素材 | m03-生成与素材 | F3.01~ | 19 | 二十九期 |
| M4 导出交付 | m04-导出交付 | F4.01~ | 10 | 二十九期 |
| M5 通知公告 | m05-通知公告 | F5.01~ | 4 | 三十期 |
| M6 组织协同 | m06-组织协同 | F6.01~ | 29 | 三十一期 |
| M7 分销提现 | m07-分销提现 | F7.01~ | 11 | 三十一期 |
| M8 充值 | m08-充值 | F8.01~ | 8 | 三十一期 |
| M9 剧本库与投稿 | m09-剧本库与投稿 | F9.01~ | 25 | 三十期 |
| M10 导演与融合 | m10-导演与融合 | F10.01~ | 28 | 三十期 |
| M11 评估与工具 | m11-评估与工具 | F11.01~ | 14 | 三十期 |
| M12 管理后台 | m12-管理后台 | F12.01~ | 39 | 三十一期 |
| M13 火花计划 | m13-火花计划 | F13.01~ | 2 | 三十期 |
附录 A2:代码域 → 分册反向索引
| backend 域包 | 分册 |
|---|---|
internal/domain/auth | m01 |
internal/domain/task | m02 |
internal/domain/generation | m03 |
internal/domain/export | m04 |
internal/domain/notification | m05 |
internal/domain/org | m06 |
internal/domain/distribution | m07 |
internal/domain/recharge | m08 |
internal/domain/scriptlib | m09 |
internal/domain/director | m10 |
internal/domain/evaluation | m11 |
internal/domain/admin + internal/domain/llm | m12 |
internal/domain/spark | m13 |
internal/domain/credits(计费横切) | 总则 §2.3 + 各分册扣费节 |
internal/infra/{hub,ids,storage} + internal/gateway | 总则 §2.2/§2.5/§2.7 |
cmd/server/routes.go(基础 3 条:/api/events、/api/health、release-mode) | 总则 §2.2/§2.5 |
internal/recovery(启动恢复四扫描) | 跨域,见总则 §2.3 退款唯一口 + m02/m03 |
附录 B:实现差异台账(DIF)
格式
DIF-<期>-<序号>;FSD 撰写期新发现逐条登记。PRD §4 已登记的移植差异不重复立目,功能节内直接引用(如「差异⑤族」)。处置列:登记(有意保真)/待裁决/已修。
| ID | 日期 | 位置 | 现象 | 影响 | 处置 |
|---|---|---|---|---|---|
| —(基线) | — | PRD §4 M1~M13 | 移植期已登记差异全集(wire 形态/时间双轨/Mock 台账 desc 参数化/估算法等) | 见 PRD §4 对应模块节 | 登记(引用不重复) |
| DIF-28-1 | 2026-10-20 | F1.01 §7(auth/service.go:121) | 注册台账行描述硬编码「注册赠送 200 积分(初始)」,而实发积分与欢迎通知文案均跟随 trial_credits settings 实际值——配置改非 200 时台账文案与实发不符 | 仅台账展示文案失真,余额正确;冻结 index.js:92 同 quirk(文案字面 200) | 登记(冻结 quirk 保真,勿修——改文案即偏离冻结对拍基准) |
| DIF-29-1 | 2026-10-20 | F2.01 §5 等(task/repo.go 多处) | 读面空列表 wire null vs 冻结恒 []——GET /tasks data、/tasks/:id assets、scenes2、shots/:id/histories、asset-library、ShotView.slots 六处(gorp nil slice) | 前端 ` | |
| DIF-29-2 | 2026-10-20 | F2.06 §7(service_gen.go:62) | extract v1 单张失败 502 文案「(<总额> 积分已退回)」用总额;冻结 v1 传单价「(10 积分已退回)」 | 仅文案金额失真,退款金额正确(单价) | 登记 |
| DIF-29-3 | 2026-10-20 | F2.06 §5(service_gen.go:128) | extract2 响应 item 与 full2 variants 合并缺 variants 键(omitempty);冻结每项显式 variants: [] | 前端消费面未读该键,零功能影响 | 登记 |
| DIF-29-4 | 2026-10-20 | F2.11 §8(task/service.go:642、service_gen.go:529) | image_histories 行 id 前缀 si_(ids.PrefixShotImage);冻结恒 uid('ih_')——ids.PrefixImageHistory="ih_" 仅 director 域消费(repo.go:319 InsertImageHistory),task 域历史行走 si_:前缀双轨并存(三十期勘正,原「已定义未用」表述不实) | GET /shots/:id/histories 行 id 形态分叉;前端零消费 | 登记(改前缀即偏离现行测试锚;前缀双轨事实随 m10 F10.06 展开) |
| DIF-29-5 | 2026-10-20 | F2.02 §4(repo.go:136-143) | legacy/duplicate INSERT 显式写 18 列零值(script_type=’’/min_duration=0/split_status=’’ 等覆盖 DDL 默认);冻结仅 5 列走 SQLite DEFAULT | wire 上 legacy/副本任务 script_type:""、min_duration:0 与冻结键值不同 | 登记 |
| DIF-29-6 | 2026-10-20 | F2.03 §7(task/service.go:263-266) | CreateV2 token 计费 ceil 向上取整;冻结 Math.round——credits.TextCost 已实现正确 round(billing.go:118-131)却未被调用(死代码并存) | 计费边界差 1 积分(est 非 tokens 整倍数时);对拍基准偏离 | 登记(修则同步回归测试期望;二十九期未动) |
| DIF-29-7 | 2026-10-20 | F2.03 §7(task_assembly.go:122-137) | V2 台账 desc 模型名可空串(textModelName() 无缺省兜底);冻结 getSetting('text_model') 缺行回「星芒文本引擎」 | 台账 desc 出现「…tokens / ,-N)」空串形态 | 登记 |
| DIF-29-8 | 2026-10-20 | F2.07 §5(task_handlers.go:524-532) | resplit 响应回显原始 instruction(trim 只在 service 内部);冻结回显 trim 后值;库存 trim 值 | 带首尾空格请求时响应与冻结 wire 不同 | 登记 |
| DIF-29-9 | 2026-10-20 | F2.09 §4(service_gen.go:596-603) | preview 台账 desc「2K预览(-15)」缺「生成」二字(冻结「2K预览生成(-15)」);Mock label 随 desc 全串(冻结 label「分镜图」/「2K预览」——影响 Mock SVG 文件名) | 台账文案+Mock 落盘文件名形态 | 登记 |
| DIF-29-10 | 2026-10-20 | F2.05 §4(service.go:183/200-204) | duplicate title 截 60(冻结不截);仅复制源镜存在 slot_no=1 的槽位(冻结每镜无条件插) | 长 title 副本与槽位缺失分叉 | 登记 |
| DIF-29-11 | 2026-10-20 | F2.10 §3(task_handlers.go:494/505) | 场景重命名/删除 groupNo 非数字→归 0 匹配零行 200;冻结 NaN 绑定 TypeError→500 | 异常路径语义分叉(Go 更宽容) | 登记 |
| DIF-29-12 | 2026-10-20 | F2 通用(各 handler) | JSON bind 失败统一 400「请求体格式错误」;冻结 express.json 语法错误走中央中间件透出 SyntaxError message 截 300 | 错误文案形态不同(中文统一 vs 异常文本透传) | 登记 |
| DIF-29-13 | 2026-10-20 | F2.02 §3(service.go:155/160) | legacy 缺省标题「未命名」;冻结 INSERT title 用「未命名剧本」而台账 desc 用「未命名」——Go 把 desc 缺省同化进 title | 缺省标题 wire 值不同 | 登记 |
| DIF-29-14 | 2026-10-20 | F2.03 §3(service.go:278-279) | V2 script_type/face_style 仅 orDefault 补空串、任意值透传;冻结二值化归一(‘overseas’?‘overseas’:‘domestic’ 等) | 非法枚举值入库透传 | 登记 |
| DIF-29-15 | 2026-10-20 | F2.03 §7(service.go:262) | token 估算用 rune 数×1.6;冻结 UTF-16 .length——BMP 外字符(emoji 等)计数不同 | 计费估算边界差(astral 字符面) | 登记 |
| DIF-29-16 | 2026-10-20 | F2.04 §4(service_gen.go:332-341) | full/full2 的 assets_data fallback 拼装收窄 5 键 item;冻结直接塞 assets 原始全列行 | 缓存缺失时降级形态不同;前端消费 5 键内无感 | 登记 |
| DIF-29-17 | 2026-10-20 | F3.02 §7(generation/model.go:19/:27) | videos standalone/refunded wire 出 bool true/false;冻结 SQLite SELECT * 透传 1/0 数字 | 前端类型未声明零消费,低危 | 登记 |
| DIF-29-18 | 2026-10-20 | F3.09 §8(generation/service.go:122-141) | contentTypeOf 缺 .mp3/.m4a/.aac/.ogg/.flac/.mp4 分支——音频上传/真实成片 Content-Type 落 default image/svg+xml | URL 直读功能面不受影响,响应头 wire 形不同 | 登记(补扩展映射为低成本改进候选) |
| DIF-29-19 | 2026-10-20 | F3.08 §4(generation/service.go:114-119) | Mock 图片落盘文件名=label+ObjectKey 随机名;冻结 img_<uid>.svg 且 uid 即行 id(文件名与 id 解耦 vs 绑定) | 文件名形态差异;url 键形不变 | 登记 |
| DIF-29-20 | 2026-10-20 | F3.06 §7(generation_handlers.go:193-196) | style-presets 有 requireDB——降级模式 503;冻结零 DB 依赖恒 200(纯目录+文件系统) | 降级面可用性收窄(faq 无 requireDB 与冻结一致) | 登记 |
| DIF-29-21 | 2026-10-20 | F3.08 §7(service_media.go:84-86) | ref_image_ids 追加错误 _ = 吞掉;冻结同语句抛错进中央 500 | 触发面仅 DB 故障 | 登记 |
| DIF-29-22 | 2026-10-20 | F3.01 §7(service_video.go:316-318) | 真实通道 LinkShotVideoKeep 失败→videoFailFast 退款+500「视频生成失败」;冻结 providers.js:103-105 shots UPDATE 在 try 外不退款走中央 500 | Go 更保守(多退一次款语义)——integration_test.go:320-323 裁决范围收窄注记引用 | 登记(裁决保真) |
| DIF-29-23 | 2026-10-20 | F4.01 §3(export_handlers.go:24/:113) | export 创建/单镜参数 _ = c.Bind 静默忽略解析错误,malformed JSON 零值续走业务(② → 404 task not found);冻结 express.json 400 SyntaxError | 错误路径语义分叉(Go 更宽容) | 登记 |
| DIF-29-24 | 2026-10-20 | F4.05 §7(export/service.go:259-260) | deliver 空 file_path 视频行跳过 200;冻结 path.basename('') → readFileSync(目录) 崩溃 500 | Go 不复刻冻结崩溃(代码注释已登记、PRD §4 M4 正文未含) | 登记(修后语义保真) |
| DIF-30-1 | 2026-10-20 | F9.04 §7(scriptlib/repo.go:713) | GetWriterJoinedAt 原生 QueryRow 对 users 行查无透传错误→500;冻结 studio.js null 兜底 | FK 语义死路径(writer_profiles.user_id 外键恒存在)——十四续 B8 注释改实在先 | 登记(不可达面) |
| DIF-30-2 | 2026-10-20 | F9.05 §8 等(scriptlib_handlers.go:119/:189/:261/:290/:309/:351/:408/:426 八处) | 域内 _ = c.Bind 静默按空载荷续走(admin create/put/cover rating/submissions/profile/review/narrations);冻结 express.json malformed→400 SyntaxError | 错误路径语义分叉(DIF-29-23 同族扩域:export→scriptlib 八端点);notify handlers :45 同形 | 登记 |
| DIF-31-1 | 2026-10-20 | F6.01/F6.04/F6.08(org/model.go:72-84/:110-125/:153-166) | members/ShareRow/assets 行 created_at 键序位次三处同族——冻结 ALTER 尾插序 vs Go struct 列序,键集相等序不同,前端按键取值无感 | wire 键序方言(M4 ① 同族先例整批登记) | 登记 |
| DIF-31-2 | 2026-10-20 | F6.11/F6.13(org/service.go:747-753/:686-696) | perm-groups/material-perms 响应 map 字母序输出(冻结展开序);且 PUT perms 传非数组时冻结 Array.isArray 判否 200 沿用旧值 vs Go echo bind []string 直接 400 | 键序方言+类型宽容差两点合一 | 登记 |
| DIF-31-3 | 2026-10-20 | F12.01/06/08/11/12/13(admin/model.go:122/:172/:205/:256 等) | 布尔 wire 型 enabled/active/is_active/refunded Go true/false vs 冻结 SQLite 0/1 | 方言级 wire 分叉(六节波及;JS truthy 消费面无感) | 登记 |
| DIF-31-4 | 2026-10-20 | F12.02/F12.04 §3(admin_handlers.go:40-44/:89-93)+F8.01 §7 扩域 | bind 严格性:冻结 +credits/+amount/+amount_cny 一元加强转宽收 JSON 数值字符串;Go int64/float64 bind 失败→400「请求体格式错误」/echo 400 裸信封 | 请求侧类型宽容差(M7 ⑥ credits 面同族扩域:m12 用户/发放面+m08 下单面) | 登记 |
| DIF-31-5 | 2026-10-20 | F12.10 §7(service_billing.go:92-109) | PUT settings 非标量值字符串化:Go jsString default fmt.Sprint 对 map→map[a:1] vs 冻结 String(v)→[object Object] | 仅 body 值为对象/数组时;存储面方言 | 登记 |
| DIF-31-6 | 2026-10-20 | F12.12 §8(service_prompts.go:244-250) | upload 点文件名:Go regexpStripExt idx<=0 保留 .md 作版本名 vs 冻结 /.[^.]+$/ 匹配整段清空→缺省名兜底 | 仅 .xxx 形态文件名边缘 | 登记 |
| DIF-32-1 | 2026-10-20 | F11.08 §5(evaluation/model.go:210-211+service_viral 列表展开) | viral list wire:冻结带 status 键且 result 为 JSON 原串直出;Go Result/Status 均 json:"-"、result 经 service 展开为对象、status 整键不出 | 三十二期双跑对账新发现(scripts/dev/parity 首扫抓获;F11.08 §5 只记了 Go 侧 json:"-" 面,未立冻结差);前端 ViralAnalysisView 自行 JSON.parse 冻结串,Go 对象形态恰兼容 | 登记 |
| DIF-32-2 | 2026-10-20 | F9.02 §5(service_studio.go:31-46 market list) | market list 行宽:冻结 15 列窄投影(id/title/genre/market/audience/form/tags/highlights/description/rating/cover_path/char_count/uses_total/uses_7d/enabled)vs Go 宽行(另含 content/created_at/episodes/planned_chars/writer_name/synopsis/setting/preview_text/characters/worldview 等裸行键) | 三十二期双跑对账新发现(同上);多余键前端零消费无感 | 登记 |
4.15 - QMTV 功能规格说明书(FSD)v1.0 · 总则
上游文档:prd-qmtv-v1.md(需求与契约)。本文档以 Go 实现代码为事实基准(
backend/,239/239 收官),按模块还原系统真实运转规则;与 PRD 的差异以> ⚠️ 差异 DIF-<批次>-<序号>注记并登记 99-附录 B。冻结版 Express(qmtv/server/)仅作差异对拍参照,不再单独展开。 功能 ID 规则:F<m>.<两位序号>(m = PRD 模块号 M1~M13)。ID 全局唯一、只增不改号;分册文件m<m>-<中文名>.md。
0. 修订记录
| 版本 | 日期 | 对应 PRD 版本 | 修订说明 | 作者 |
|---|---|---|---|---|
| v1.0 | 2026-10-20 | prd-qmtv-v1(v1.14) | 骨架批:总则(术语/全局规则/模块清单)+ 99-附录(对照表/DIF 台账)+ 首批分册 M1 认证与用户(F1.01~,标杆册) | — |
| v1.1 | 2026-10-20 | prd-qmtv-v1(v1.14) | 创作主链三册:m02-任务与拆解(38)/m03-生成与素材(19)/m04-导出交付(10);§2.3 计费链路 Go 实名化+评估规则下沉分册、§2.7 前缀示例勘正(t_/shot_);DIF-29-1~24 入台账 | — |
| v1.2 | 2026-10-20 | prd-qmtv-v1(v1.14) | 工具生态五册:m09-剧本库与投稿(25)/m10-导演与融合(28)/m11-评估与工具(14)/m05-通知公告(4)/m13-火花计划(2);DIF-30-1~2 入台账(m09);§2.5 video 四状态帧勘正 | — |
| v1.3 | 2026-10-20 | prd-qmtv-v1(v1.14) | 商业管理四册:m06-组织协同(29)/m12-管理后台(39)/m07-分销提现(11)/m08-充值(8)——13 册全量收官;反向覆盖收口(场景清单「FSD 功能 ID」列回填+人工清单对账+分区页入口卡挂出);DIF-31-1~6 入台账 | — |
0.1 文档定位
- PRD 管 to-be 契约(需求意图、模块边界、API 清单唯一权威);FSD 记 as-is 实现(系统当前真实行为,以 Go 代码逐字取证)。
- 每个功能节标注「对应 PRD」模块;实现与 PRD 不一致处行内差异注记并登记附录 B(现状 file:line / PRD 期望 / 影响面)。PRD §4 已登记的实现差异不重复立 DIF 条目,节内引用即可——DIF 台账只收 FSD 撰写期新发现。
- 分册结构(沿 pms fsd-pms 形态):
m01~m13共 13 册 + 本总则 + 99-附录;跨文件一律以 F-ID 纯文本引用。 - 场景挂钩:功能节元数据行标注对应 E2E 场景 ID(
docs/test/e2e-qmtv-场景清单.md),三十一期反向覆盖收口时回填场景清单「FSD 功能 ID」列。
1. 术语与数据字典
收录判据:PRD、界面文案、代码命名三方叫法不一致的词必收。表名列名权威在 数据库表设计-qmtv.md(保留冻结原名),此处只收易混术语。
| 界面/PRD 叫法 | 代码命名 | 本文统一叫法 | 说明 |
|---|---|---|---|
| 积分 | credits(users 列)/ credit_records | 积分 / 积分台账 | 整数语义,二十一期 ⑥ 起 DECIMAL(12,2) 存储、读侧 float64 |
| 拆解 | split / preprocess(旧版叫预处理) | 拆解 | v2 token 制(1000 tokens≈1 积分)为现行;旧版按次 20 积分仍在 |
| 镜数 | shotCount(wire 驼峰)/ shots 表 | 镜数 | wire 是驼峰(冻结 index.js:230 保真),误读蛇形 shot_count 是二十六期 A1 回归根因 |
| 分镜 | shot / director_shots 双表 | 分镜 | 双工作台双表通路(shot._tbl 约定),分册内标注归属表 |
| 火花计划 | spark / spark_submissions | 火花 | 三阶梯 100/150/200,加入奖励 +100 |
| 台账类型 | credit_records.type | 台账类型 | TypeCredit 等常量族(credits/model.go),描述文案「模块·动作(-N)」格式 |
| 工作台 | workbench(工具域)/ dashboard(拆解台) | 工作台工具 / 拆解台 | 界面两个「工作台」指向不同域:/workbench=M11 工具,/dashboard=M2 拆解 |
| 独立生成 | standalone(video/images 独立路由) | 独立生成 | M3 域内概念:不经任务链直出图片/视频 |
| 评估冻结-结算 | reserve(TypeReserve 台账) | 评估冻结 | 提交时冻结 100/300,完成结算、失败全额退 |
2. 全局规则
2.1 权限模型
| 层 | 载体 | 覆盖面 |
|---|---|---|
| 登录态 | AuthMiddleware(gateway/auth.go:120,全站默认) | 除公开面(login/register/health/style-presets/faq/showcase 公开段等)外全部端点 |
| 管理员 | AdminMiddleware(gateway/auth.go:133,校验 role=‘admin’) | /admin/* 面与 M12 全域;非 admin 403 |
| 组织属主 | org 域内 ownerOnly / adminOrOwner 两判定(勿混用) | M6 组织面:owner target 特判(如 material-perms「所有者默认拥有全部权限」400) |
| 数据属主 | 各域 service 内 user_id 过滤(WHERE 拼接) | 越权访问统一 404「资源不存在」形态(不暴露存在性),个别 403(组织/导演面) |
三角色入口可见性(前端):普通用户无管理后台入口且直访 /admin 被路由守卫顶回;组织功能导航按 org_id 条件可见。
2.2 错误信封与哨兵
- 统一信封:成功
200 {"ok":true,"data":<业务体>}(gateway.OK,middleware.go:44)/ 失败<状态码> {"error":"<中文哨兵文案>"}(gateway.Err,middleware.go:52——错误键是error非message,文案截 300 rune 镜像冻结slice(0,300));HTTP 状态码语义化(400/401/402/403/404/500/502/503)。 - 哨兵机制:各域定义哨兵 error(如
task.ErrNotFound),errRespond(auth_handlers.go:39)集中映射哨兵→HTTP 状态+文案。文案逐字节对齐冻结版(前端直接展示 message)。 - 关键横切状态码:积分不足=
402(credits.ErrInsufficient→MsgInsufficientCrd);降级模式业务请求=503「数据库未配置」(requireDB);生成 Provider 失败=502(内部已退款)。 - 404 vs 400 取舍原则:属主不存在/无权访问→404(不暴露存在性);参数校验失败/状态不合法→400 带动态文案(如 distribution「不允许从「X」流转到「Y」」)。
2.3 计费唯一链路(三条铁律之二)
flowchart LR
A["业务动作"] --> B["credits.Service.TryDeduct<br/>(冻结 util.tryDeduct)<br/>基础价 COSTS + settings 可调双层"]
B -->|余额不足| C["402 ErrInsufficient"]
B -->|扣成功| D["generation.Service.SmartVideo / provider.GenerateImage<br/>(冻结 providers.videoSmart/imageSmart)<br/>真实供应商或内置 Mock"]
D -->|失败| E["generation 内部统一退款 RefundGenError<br/>调用方不得重复退款"]
D -->|成功| F["产物落库 + 台账 credit_records"]- 基础价(credits/billing.go):图 10 / 2K 预览 15 / 独立视频 50 / 重新拆解 20 / 爆款 5;token 制 1000 tokens≈1 积分;注册赠 200。settings 键可调项以管理后台「计费配置」为准。
- 台账:每笔扣/退一行
credit_records(type 常量 + 描述「模块·动作(-N)」);域内台账流转(如评估冻结-结算)归各分册(§1 词条兜底)。 - 退款唯一口在生成域内部(
RefundGenError文案组装 +ClaimVideoRefund防重认领)与各域显式退款函数(二十一期 ③ Refund 收口);审批到账(充值)走 credits.Add。
2.4 Mock 优先架构
- 未配置模型接入时全站走内置 Mock:前端项目零环境变量接 AI;真实供应商由管理后台
model_endpoints表配置(OpenAI 兼容),字段不全自动回退 Mock。 - Mock 行为是契约的一部分(同步 done、确定性输出、进度事件照发),e2e 断言册全量依赖;分册内 Mock 分支行为照实登记,不标「临时代码」。
2.5 SSE 事件总线
GET /api/events(query token 兜底)单进程事件总线,五类事件(notification/hub.go:21-25):video(生成进度)/split(拆解 done/fallback)/recharge(充值到账,仅发本人)/workbench(工作台 done/failed)/eval(评估 stage/done/failed)。审片/通知无 SSE(前端轮询)。分册内「零 SSE 零通知」标注=该域刻意不走事件面。
2.6 认证与 token
- 自实现 HMAC JWT + refreshToken;
QMTV_JWT_SECRET环境变量优先,缺省持久化data/.jwt_secret(与冻结版同文件,存量 token 跨运行时有效)。 - refresh 真轮换(F1):
POST /api/auth/refresh返回新 refreshToken,旧 token 复刷 401——与冻结版差异(冻结 refresh 恒 401 无新字段),已登记 PRD 差异⑤族。 - query token 兜底仅两处:SSE
/api/events?token=、GET /api/images/:id?token=(EventStream/img 标签带不了自定义头)。 - 时间形态双轨(刻意不同源勿统一):auth 域
LocalTime(注册/登录时间,冻结dayjs本地形态)vs 审批类reviewed_atUTC。
2.7 wire 形态与 ID 前缀
- wire 键按域登记不全局统一:任务/生成域驼峰(
shotCount/taskType)、spark me 投影蛇形(next_tier)、导出端点蛇形——前端按域消费,改名即回归(shotCount 事故教训)。分册内端点响应示例照实标键形。 - 业务 ID 一律
ids.go前缀 + 随机段(51 前缀全量对拍守卫钉死:u_/t_/shot_/as_/ro_/wk_…),前缀即域归属判别。
3. 功能模块清单
| 模块 | 冻结源文件 | 端点数 | F-ID 段 | 分册 | 状态 |
|---|---|---|---|---|---|
| 基础 | index.js(SSE/health/release-mode) | 3 | —(横切面,见总则 §2.2/§2.5) | 无独立分册 | 总则已覆盖 |
| M1 认证与用户 | index.js(认证段) | 9 | F1.01~ | m01-认证与用户 | 二十八期首落 |
| M2 任务与拆解 | index.js + routes2/3(task 段) | 38 | F2.01~ | m02-任务与拆解 | 二十九期首落 |
| M3 生成与素材 | index.js + routes2/3(生成段) | 19 | F3.01~ | m03-生成与素材 | 二十九期首落 |
| M4 导出交付 | index.js + routes2/3(导出段) | 10 | F4.01~ | m04-导出交付 | 二十九期首落 |
| M5 通知公告 | index.js | 4 | F5.01~ | m05-通知公告 | 三十期首落 |
| M6 组织协同 | index.js(组织段)+ enterprise.js | 29 | F6.01~ | m06-组织协同 | 三十一期首落 |
| M7 分销提现 | index.js(:598-:638/:708-:737) | 11 | F7.01~ | m07-分销提现 | 三十一期首落 |
| M8 充值 | recharge.js | 8 | F8.01~ | m08-充值 | 三十一期首落 |
| M9 剧本库与投稿 | scriptlib.js + studio.js | 25 | F9.01~ | m09-剧本库与投稿 | 三十期首落 |
| M10 导演与融合 | director.js + fusion.js | 28 | F10.01~ | m10-导演与融合 | 三十期首落 |
| M11 评估与工具 | evaluation.js + workbench.js + viral | 14 | F11.01~ | m11-评估与工具 | 三十期首落 |
| M12 管理后台 | index.js(admin) + llmroutes.js + routes2(admin) | 39 | F12.01~ | m12-管理后台 | 三十一期首落 |
| M13 火花计划 | index.js:739-766 | 2 | F13.01~ | m13-火花计划 | 三十期首落 |
端点合计:基础 3 + 9+38+19+10+4+29+11+8+25+28+14+39+2 = 239(对账 PRD §3 合计行——注意「基础」3 条〔SSE 事件流/健康检查/发布模式〕是独立行,M1M13 直加 236,勿漏)。分批:二十九期 M2M4(创作主链 67)/三十期 M5+M9M11+M13(工具生态 73)/三十一期 M6M8+M12(商业管理 87)+反向覆盖收口——13 册全量收官。