From 9707b72808fdb68ccdd8eea179fc8701e39f6526 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Thu, 13 Aug 2026 19:55:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(ECR-041):=20OpsCMS=20Banner=20=E8=96=84?= =?UTF-8?q?=E5=86=99=E9=9D=A2=EF=BC=88Write-Wave=20=E9=A6=96=E5=88=80?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin POST/PUT + admin.cms.write/审计;C 端 GET /home/banners;首页投影回退静态 homeFeeds;migration 000051;不碰 FeedSlot/支付/UGC。 Co-authored-by: Cursor --- .ai/product/feature-map.md | 2 +- .ai/product/feature-spec/README.md | 3 +- .ai/product/feature-spec/ops-banner-write.md | 112 ++++++++++++ .ai/product/feature-spec/ops-banner.md | 11 +- .ai/product/feature-spec/ops-content.md | 2 +- apps/admin-h5/src/api/client.ts | 45 +++++ apps/admin-h5/src/pages/CMSPage.vue | 123 +++++++++++-- apps/api/internal/handler/admin_cms.go | 65 +++++++ apps/api/internal/handler/home.go | 11 ++ apps/api/internal/httpserver/router.go | 1 + .../integration/cms_banner_write_test.go | 137 +++++++++++++++ .../internal/repository/cms_banner_write.go | 162 ++++++++++++++++++ apps/api/internal/service/admin/cms_write.go | 110 ++++++++++++ apps/api/internal/service/admin/rbac.go | 3 +- apps/api/internal/service/home/service.go | 19 ++ .../000051_ops_banner_write.down.sql | 5 + .../migrations/000051_ops_banner_write.up.sql | 7 + apps/user-h5/src/composables/useHomePage.ts | 14 +- apps/user-h5/src/lib/homeCatalog.spec.ts | 12 +- apps/user-h5/src/lib/homeCatalog.ts | 26 +++ .../BD-2026-041-banner-write.md | 37 ++++ docs/CHANGELOG.md | 4 +- docs/CODE_REVIEW/ECR-041.md | 17 ++ docs/CONTRACT_DIFF/ECR-041.yaml | 27 +++ docs/ECR/ECR-041-banner-write.md | 54 ++++++ docs/HANDOFF/ECR-041-architect-to-engineer.md | 24 +++ docs/HANDOFF/ECR-041-engineer-to-reviewer.md | 4 + docs/PRODUCT_SPEC/ECR-041-banner-write.md | 5 + docs/PROJECT_PROFILE.md | 9 +- docs/STATE/ECR-041.md | 10 ++ docs/TEST_REPORT/ECR-041.md | 35 ++++ docs/TRACEABILITY.md | 6 +- docs/WAVE0/WRITE_WAVE_AUTHORIZATION.md | 70 ++++++++ packages/sdk/src/index.ts | 13 ++ proto/openapi.yaml | 47 +++++ 35 files changed, 1203 insertions(+), 29 deletions(-) create mode 100644 .ai/product/feature-spec/ops-banner-write.md create mode 100644 apps/api/internal/integration/cms_banner_write_test.go create mode 100644 apps/api/internal/repository/cms_banner_write.go create mode 100644 apps/api/internal/service/admin/cms_write.go create mode 100644 apps/api/migrations/000051_ops_banner_write.down.sql create mode 100644 apps/api/migrations/000051_ops_banner_write.up.sql create mode 100644 docs/BACKEND_DESIGN/BD-2026-041-banner-write.md create mode 100644 docs/CODE_REVIEW/ECR-041.md create mode 100644 docs/CONTRACT_DIFF/ECR-041.yaml create mode 100644 docs/ECR/ECR-041-banner-write.md create mode 100644 docs/HANDOFF/ECR-041-architect-to-engineer.md create mode 100644 docs/HANDOFF/ECR-041-engineer-to-reviewer.md create mode 100644 docs/PRODUCT_SPEC/ECR-041-banner-write.md create mode 100644 docs/STATE/ECR-041.md create mode 100644 docs/TEST_REPORT/ECR-041.md create mode 100644 docs/WAVE0/WRITE_WAVE_AUTHORIZATION.md diff --git a/.ai/product/feature-map.md b/.ai/product/feature-map.md index e562962..50fba9f 100644 --- a/.ai/product/feature-map.md +++ b/.ai/product/feature-map.md @@ -238,7 +238,7 @@ UI **不出现「塔罗」**。禁止神谕吉凶、恐吓话术。 | Phase D `[Ops]` | 商业加深:订单筛选 · 展示价 · 退款只读(**ECR-009** · Spec `ops-commerce.md`) | | Phase E `[Ops]` | 系统:RBAC · 封禁 · 推送占位(**ECR-010** · Spec `ops-system.md`) | | Phase D+ Contract | **Contract-First** 扩展目录:**ECR-013A…040**(RBAC/兑换码/问答运营/安全/危机/CMS/ExploreConfig 等)。见 `docs/WAVE0/` | -| Phase F+ | 封禁细策略 · 真推送通道 · 更细权限矩阵;**UGC 仍 `[No]`**;真支付最后 | +| Phase F+ / Write-Wave | **写面加深**(首刀 **ECR-041 Banner** · `docs/WAVE0/WRITE_WAVE_AUTHORIZATION.md`);封禁细策略 · 真推送;**UGC 仍 `[No]`**;**真支付最后** | 不计入 P1 Complete;不进入五 Tab。 diff --git a/.ai/product/feature-spec/README.md b/.ai/product/feature-spec/README.md index 7a5f2be..498af49 100644 --- a/.ai/product/feature-spec/README.md +++ b/.ai/product/feature-spec/README.md @@ -36,7 +36,8 @@ | [ops-ai-core-config.md](ops-ai-core-config.md) | AI 核心配置 AICoreConfig | §7 | `admin-h5` `/ai` | Ops · **ECR-021** | | [ops-crisis-care.md](ops-crisis-care.md) | 危机关怀 CrisisCare | §7 | `admin-h5` `/crisis` | Ops · **ECR-022** | | [ops-knowledge-source.md](ops-knowledge-source.md) | AI 知识源 KnowledgeSource | §7 | `admin-h5` `/ai` | Ops · **ECR-023** | -| [ops-banner.md](ops-banner.md) | OpsCMS Banner | §7 | `admin-h5` `/cms` | Ops · **ECR-024** | +| [ops-banner.md](ops-banner.md) | OpsCMS Banner(只读基线) | §7 | `admin-h5` `/cms` | Ops · **ECR-024** | +| [ops-banner-write.md](ops-banner-write.md) | OpsCMS Banner **写面** | §7 | admin CMS 写 · `GET /home/banners` | Write-Wave · **ECR-041** | | [ops-feed-slot.md](ops-feed-slot.md) | OpsCMS FeedSlot | §7 | `admin-h5` `/cms` | Ops · **ECR-025** | | [ops-scheduled-publication.md](ops-scheduled-publication.md) | OpsCMS ScheduledPublication | §7 | `/admin/cms/publications*` | Ops · **ECR-026** | | [ops-knowledge-chunk.md](ops-knowledge-chunk.md) | AICoreConfig KnowledgeChunk | §7 | `/admin/ai/knowledge-chunks*` | Ops · **ECR-027** | diff --git a/.ai/product/feature-spec/ops-banner-write.md b/.ai/product/feature-spec/ops-banner-write.md new file mode 100644 index 0000000..971f8e2 --- /dev/null +++ b/.ai/product/feature-spec/ops-banner-write.md @@ -0,0 +1,112 @@ +# 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` diff --git a/.ai/product/feature-spec/ops-banner.md b/.ai/product/feature-spec/ops-banner.md index d03e306..70d88ac 100644 --- a/.ai/product/feature-spec/ops-banner.md +++ b/.ai/product/feature-spec/ops-banner.md @@ -1,13 +1,14 @@ -# Feature Spec: OpsCMS · Banner(Ops · ECR-024) +# Feature Spec: OpsCMS · Banner(Ops · ECR-024 只读基线) -> Status: `Active`(Loop continuous · **ECR-024 Closed**) +> Status: `Active`(**ECR-024 Closed** · 只读) > Parent: WAVE0-FROZEN · Predecessor: ECR-023 Closed > Capability: `OpsCMS` · BC: `Ops_CMS_NoUGC` -> 授权:`docs/WAVE0/LOOP_AUTHORIZATION.md` +> 授权(只读队列):`docs/WAVE0/LOOP_AUTHORIZATION.md` +> **写面:** [ops-banner-write.md](ops-banner-write.md)(**ECR-041** · Write-Wave) -## Non-goals +## Non-goals(本只读 Spec) -Banner 写发布 · FeedSlot · ScheduledPublication · UGC · 真支付 +Banner 写发布(见 ECR-041)· FeedSlot · ScheduledPublication · UGC · 真支付 ## L2 Domain diff --git a/.ai/product/feature-spec/ops-content.md b/.ai/product/feature-spec/ops-content.md index 0558671..231ace7 100644 --- a/.ai/product/feature-spec/ops-content.md +++ b/.ai/product/feature-spec/ops-content.md @@ -27,7 +27,7 @@ | In | Out | |---|---| -| 首页宫格 CRUD(最多约 24 项;默认 12) | 首页 Feeds 信息流 CMS(本 ECR 后置,仍用静态 `homeFeeds`) | +| 首页宫格 CRUD(最多约 24 项;默认 12) | 首页 Feeds/推荐区运营:ECR-008 后置;**Banner 写面见 ECR-041**;完整 FeedSlot 写另 ECR | | 工具 path / icon 白名单校验 | 任意外链、自定义 SVG 上传 | | scales 列表 + 上架/下架 + 审计 | 新建量表题目编辑器 · 探索目录全量 CMS | | Admin「内容」页 | RBAC / 封禁 / 推送(Ops-E) | diff --git a/apps/admin-h5/src/api/client.ts b/apps/admin-h5/src/api/client.ts index ad44e0c..9e5d0fd 100644 --- a/apps/admin-h5/src/api/client.ts +++ b/apps/admin-h5/src/api/client.ts @@ -452,6 +452,51 @@ export const adminApi = { system: boolean updated_at: string }>('GET', `/cms/banners/${id}`), + createBanner: (body: { + code: string + title: string + placement: string + image_url?: string | null + link_path?: string | null + sort_order: number + active: boolean + }) => + request<{ + id: string + code: string + title: string + placement: string + image_url?: string + link_path?: string + sort_order: number + active: boolean + system: boolean + updated_at: string + }>('POST', '/cms/banners', body), + updateBanner: ( + id: string, + body: { + code: string + title: string + placement: string + image_url?: string | null + link_path?: string | null + sort_order: number + active: boolean + }, + ) => + request<{ + id: string + code: string + title: string + placement: string + image_url?: string + link_path?: string + sort_order: number + active: boolean + system: boolean + updated_at: string + }>('PUT', `/cms/banners/${id}`, body), feedSlots: () => request<{ items: Array<{ diff --git a/apps/admin-h5/src/pages/CMSPage.vue b/apps/admin-h5/src/pages/CMSPage.vue index de8a86e..4c82833 100644 --- a/apps/admin-h5/src/pages/CMSPage.vue +++ b/apps/admin-h5/src/pages/CMSPage.vue @@ -1,5 +1,5 @@