# Feature Spec: OpsCMS · Banner 写面(Ops · ECR-041) > Status: `Active`(**ECR-041 Closed** · Feature Complete) > Map: `§7 运营后台` · Capability: `OpsCMS` · BC: `Ops_CMS_NoUGC` > Parent: WAVE0-FROZEN · Write-Wave: `docs/WAVE0/WRITE_WAVE_AUTHORIZATION.md` > Predecessor: ECR-024 Closed(Banner 只读) > 关联:[ops-banner.md](ops-banner.md)(只读基线)· [ops-content.md](ops-content.md) · [home.md](home.md) --- ## 1. 目标 让运营可 **创建 / 更新 / 上下架** Banner;C 端首页对 `placement=home` 的 active Banner **生效**;写操作鉴权 + 审计;失败可回退静态。 验证 Write-Wave 首刀闭环,**不**做 FeedSlot、真支付、UGC。 ## 2. In / Out | In | Out | |----|-----| | Admin:Banner create / update / 设 `active` | **FeedSlot** 一切写与 API | | 加法权限 `admin.cms.write`(既有 RBAC) | soft-delete 物理删行(`system=true` 禁止删) | | C 端:`GET` active banners(至少 `placement=home`) | ScheduledPublication 写 · 外链任意 URL | | 首页推荐区:API Banner 投影优先,空则回退静态 `homeFeeds` | 把 FeedSlot 与 Banner 合并为一 ECR | | `admin_audit_logs` 记录写操作 | Crisis / Handoff / ContentSafety 写 · 真支付 · UGC | | OpenAPI · contract_diff · integration | 新权限子系统 · 改引擎分层 | ### 与 `homeFeeds` 的关系(诚实边界) - ECR-008 将「首页 Feeds CMS」后置;表意信息流位属 **FeedSlot**(ECR-025 只读,写面另 ECR)。 - **本切片**:用 **Banner(placement=home)** 驱动首页推荐区展示(投影字段见下);**不**写 `ops_feed_slots`。 - 完整 Feeds 槽位运营(多 slot、排期)→ 后续 FeedSlot 写面 ECR。 ## 3. Domain | 概念 | 语义 | |------|------| | `Banner` | `code` 唯一;`placement` ∈ {home,explore,ask};`active`;`system` | | 写语义 | upsert 字段;`active=false` = 下架;禁止 soft-delete 列/物理删 system 行 | | C 端投影 | 仅 `active=true`;按 `sort_order` 升序 | ### 首页投影映射(Banner → 原 homeFeeds 卡) | homeFeeds 字段 | 来源 | |----------------|------| | `to` | `link_path`(须站内 `/` 相对路径) | | `title` | `title` | | `meta` | 可选:无则空串或固定「运营推荐」 | | `icon` / `tone` / `stat` / `tag` | 本切片可用缺省;**不**扩表除非 migration 明确(默认缺省,避免 scope creep) | ## 4. API ### Admin(既有前缀 `/api/v1/admin/cms`) | Method | Path | 权限 | 语义 | |--------|------|------|------| | GET | `/banners` · `/banners/{id}` | `admin.cms.read` | 保持 ECR-024 | | POST | `/banners` | `admin.cms.write` | 创建(code 唯一) | | PUT | `/banners/{id}` | `admin.cms.write` | 更新 title/placement/image_url/link_path/sort_order/active | | — | DELETE | — | **不做**(下架用 `active=false`) | 校验:`link_path` 若非空须以 `/` 开头、禁 `http(s):`;`placement` 枚举;`system=true` 行不可改 `code`、不可删。 ### C 端 | Method | Path | Auth | 语义 | |--------|------|------|------| | GET | `/api/v1/home/banners` | DeviceAuth(与 `/home/tools` 同级) | 仅 `active`;query `placement` 默认 `home` | ## 5. Permission / Audit - Migration 授予 `super_admin`:`admin.cms.write`(加法码,非新 RBAC 模型)。 - 每次 POST/PUT 写 `admin_audit_logs`(actor · action · target id/code · diff 摘要)。 ## 6. Migration - 若仅需权限种子:`000051_ops_banner_write`(以 TRACEABILITY Max+1 为准,编码时再锁)。 - **表结构**:默认复用 `ops_banners`;本切片 **不**为 FeedSlot 改表。若投影缺省不够且 Human 未扩 scope,不得私自加列。 ## 7. UI | 端 | 行为 | |----|------| | admin-h5 `/cms` | Banner 列表 + 新建/编辑/上下架(只动 Banner;FeedSlot 仍只读展示可保留) | | user-h5 首页 | `GET /home/banners` → 推荐区;失败或空 → 静态 `homeFeeds` | ## 8. AC | ID | Then | |----|------| | AC-F-01 | POST 合法 Banner → list/get 可见 | | AC-F-02 | PUT `active=false` → C 端 list 不含 | | AC-F-03 | PUT `active=true` + placement=home → `GET /home/banners` 含该项 | | AC-F-04 | 重复 `code` → 4xx | | AC-F-05 | 非法 `link_path`(外链)→ 4xx | | AC-S-01 | 无 Admin → 401 | | AC-S-02 | 仅 read 无 write → POST/PUT 403 | | AC-A-01 | 写操作产生审计行 | | AC-C-01 | H5 首页:有 active home Banner 时推荐区非纯静态 | | AC-C-02 | API 失败时首页不白屏(回退静态) | | AC-O-01 | 不触碰 FeedSlot 写路径 · 无 soft-delete · 无支付 | ## 9. Non-goals(STOP 对齐) FeedSlot 写 · ScheduledPublication 写 · UGC · 真支付 · soft-delete · Crisis/Handoff/ContentSafety 写 · 扩大为本 ECR 合并多概念。 ## 10. Trace - ECR: `docs/ECR/ECR-041-banner-write.md` - BD: `docs/BACKEND_DESIGN/BD-2026-041-banner-write.md` - PRODUCT_SPEC · CONTRACT_DIFF · TEST_REPORT · CODE_REVIEW(实现阶段) - Auth: `docs/WAVE0/WRITE_WAVE_AUTHORIZATION.md`