这是本节的多页打印视图。 .
功能规格(FSD)
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)。
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=’’ 谓词引 ⑮ 族)。
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 - 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。
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)在位。
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 键收窄。
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 裁决范围收窄注记引用)。
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 正文未含)。
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 均双方一致照抄)。
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。
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。
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 判定在外层」——纯谓词重排真值表同构无副作用差异,不立目。
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。
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 等裸行键) | 三十二期双跑对账新发现(同上);多余键前端零消费无感 | 登记 |
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 册全量收官。