# FSD M12 · 管理后台

LLMS 索引： [llms.txt](/llms.txt)

---

> [← 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/) ｜ [附录 →](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/) ｜ 上游：[PRD §4 M12](/qmtv/product/prd-qmtv-v1/)
> 实现落点：`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 双挂**（403 `forbidden`，gateway/auth.go:133-141=冻结 adminRequired 逐字）；**前端消费面≠端点族**——AdminView 14 tab（AdminView.vue:667-682）中 submissions/withdrawals/scripts 三 tab 消费 **M9/M7 域**端点，本册 39 条按后端功能族 A~H 组聚合；**404 八态八哨兵刻意不同源**（model.go:16-36：用户调整英文 "not found"/密码重置中文「用户不存在」/org approve 英文/指令 DELETE 英文/提示词删「版本不存在」/接入「接入不存在」/release「任务不存在或不是失败态」/重拆「任务不存在」）；bind 失败统一 400「请求体格式错误」（与 m08 裸传 return err 两域不同制）；`api_key` `json:"-"` masked-only（二十一期②收敛，PRD ④）；**GetActivePrompt（整行+split 三级）↔activePromptContent（content-only 单级）两份同语义并存**（二十二期 C1 裁决 a，task_assembly.go:188-189 注释「两处改动须同步对拍」）。PRD §4 M12 已登记差异 ①~⑨（prd-qmtv-v1.md:222）引用不重立。

### 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_<purpose>)**。Activate：UpsertSetting → 返回 purpose。Test：三分支（text→chat/completions max_tokens 8 reply 截 30/image→images/generations 红圆判 has/video→GET base 可达）；超时 45s → TestError{504「连接超时」}/502（err 截 300）。

##### 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。

---

反链：

- [QMTV 功能规格说明书（FSD）v1.0 · 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/)
- [FSD 附录 · PRD 对照 / 代码域索引 / 差异台账](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/)
