# FSD M6 · 组织协同

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 M6](/qmtv/product/prd-qmtv-v1/)
> 实现落点：`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·dashboard `ok(null)`｜folders·groups·perm-groups·material-perms·shared-projects `ok([])`｜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 重出（`perms` `json:"-"` 串解析为数组）。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。

---

反链：

- [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/)
