From e32466357df37838d18aba0148a8de4363bff523 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sat, 8 Aug 2026 03:08:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(ECR-025):=20OpsCMS=20FeedSlot=20=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E5=B9=B6=20Closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 栏目位目录(ops_feed_slots + /cms),复用 admin.cms.read。 Co-authored-by: Cursor --- .ai/product/feature-spec/README.md | 1 + .ai/product/feature-spec/ops-feed-slot.md | 41 +++++ apps/admin-h5/src/api/client.ts | 24 +++ apps/admin-h5/src/pages/CMSPage.vue | 68 +++++++- apps/api/internal/handler/admin_cms.go | 29 ++++ .../integration/cms_feed_slot_test.go | 95 +++++++++++ apps/api/internal/repository/cms_repo.go | 51 ++++++ apps/api/internal/service/admin/cms.go | 22 +++ .../migrations/000026_ops_feed_slots.down.sql | 1 + .../migrations/000026_ops_feed_slots.up.sql | 17 ++ docs/BACKEND_DESIGN/BD-2026-025-feed-slot.md | 23 +++ docs/CHANGELOG.md | 1 + docs/CODE_REVIEW/ECR-025.md | 8 + docs/CONTRACT_DIFF/ECR-025.yaml | 22 +++ docs/ECR/ECR-025-feed-slot.md | 15 ++ docs/ENGINEERING_SPEC/ECR-025-feed-slot.md | 6 + docs/HANDOFF/ECR-025-architect-to-engineer.md | 3 + docs/HANDOFF/ECR-025-engineer-to-reviewer.md | 3 + docs/PRODUCT_SPEC/ECR-025-feed-slot.md | 3 + docs/STATE/ECR-025.md | 6 + docs/TASKS/TASK-025-ECR025.yaml | 12 ++ docs/TEST_REPORT/ECR-025.md | 33 ++++ docs/TRACEABILITY.md | 1 + docs/WAVE0/LOOP_AUTHORIZATION.md | 2 +- proto/openapi.yaml | 28 ++++ scripts/ess-slice-close.py | 150 ++++++++++++++++++ 26 files changed, 661 insertions(+), 4 deletions(-) create mode 100644 .ai/product/feature-spec/ops-feed-slot.md create mode 100644 apps/api/internal/integration/cms_feed_slot_test.go create mode 100644 apps/api/migrations/000026_ops_feed_slots.down.sql create mode 100644 apps/api/migrations/000026_ops_feed_slots.up.sql create mode 100644 docs/BACKEND_DESIGN/BD-2026-025-feed-slot.md create mode 100644 docs/CODE_REVIEW/ECR-025.md create mode 100644 docs/CONTRACT_DIFF/ECR-025.yaml create mode 100644 docs/ECR/ECR-025-feed-slot.md create mode 100644 docs/ENGINEERING_SPEC/ECR-025-feed-slot.md create mode 100644 docs/HANDOFF/ECR-025-architect-to-engineer.md create mode 100644 docs/HANDOFF/ECR-025-engineer-to-reviewer.md create mode 100644 docs/PRODUCT_SPEC/ECR-025-feed-slot.md create mode 100644 docs/STATE/ECR-025.md create mode 100644 docs/TASKS/TASK-025-ECR025.yaml create mode 100644 docs/TEST_REPORT/ECR-025.md create mode 100755 scripts/ess-slice-close.py diff --git a/.ai/product/feature-spec/README.md b/.ai/product/feature-spec/README.md index d61e4c1..f7e62d8 100644 --- a/.ai/product/feature-spec/README.md +++ b/.ai/product/feature-spec/README.md @@ -41,3 +41,4 @@ **P2 三模块队列(设计立项 / 编码另批):** [P2-BACKLOG.md](P2-BACKLOG.md) **竞品逆向(测测前端全量):** [cece-frontend-re/](cece-frontend-re/README.md) · **完整设计包:** [cece-frontend-re/complete-design/](cece-frontend-re/complete-design/README.md) · 方法见 [../../design/reverse-engineering-spec.md](../../design/reverse-engineering-spec.md) +| [ops-feed-slot.md](ops-feed-slot.md) | OpsCMS FeedSlot | §7 | `admin-h5` `/cms` · `GET /admin/cms/feed-slots*` | Ops-D · **ECR-025 Closed** | diff --git a/.ai/product/feature-spec/ops-feed-slot.md b/.ai/product/feature-spec/ops-feed-slot.md new file mode 100644 index 0000000..1a061f6 --- /dev/null +++ b/.ai/product/feature-spec/ops-feed-slot.md @@ -0,0 +1,41 @@ +# Feature Spec: OpsCMS · FeedSlot(Ops · ECR-025) + +> Status: `Active`(Loop continuous · **ECR-025 Closed**) +> Parent: WAVE0-FROZEN · Predecessor: ECR-024 Closed +> Capability: `OpsCMS` · BC: `Ops_CMS_NoUGC` +> 授权:`docs/WAVE0/LOOP_AUTHORIZATION.md` + +## Non-goals + +FeedSlot 写发布 · ScheduledPublication · UGC · 真支付 · Banner 写 + +## L2 Domain + +| 概念 | 语义 | +|------|------| +| `FeedSlot` | 本切片只读目录;code 唯一(若适用) | + +## L3 API + +| Method | Path | 权限 | 语义 | +|--------|------|------|------| +| GET | `/admin/cms/feed-slots` | `admin.cms.read` | 只读 | +| GET | `/admin/cms/feed-slots/{id}` | `admin.cms.read` | 只读 | + +## Migration + +`000026`:表 + 种子(若有)(权限复用) + +## L4 AC + +| ID | Then | +|----|------| +| AC-F-01 | list 含种子或空列表合法 | +| AC-F-02 | 已知 id get 200 | +| AC-F-03 | 未知 id → 404 | +| AC-S-01 | 无 Admin → 401 | +| AC-S-02 | 无权限 → 403 | +| AC-P-01 | list < 500ms | +| AC-O-01 | N/A 只读 | + +contract_diff: `docs/CONTRACT_DIFF/ECR-025.yaml` diff --git a/apps/admin-h5/src/api/client.ts b/apps/admin-h5/src/api/client.ts index b7d7cf8..ccd12eb 100644 --- a/apps/admin-h5/src/api/client.ts +++ b/apps/admin-h5/src/api/client.ts @@ -413,6 +413,30 @@ export const adminApi = { system: boolean updated_at: string }>('GET', `/cms/banners/${id}`), + feedSlots: () => + request<{ + items: Array<{ + id: string + code: string + title: string + slot_key: string + placement: string + active: boolean + system: boolean + updated_at: string + }> + }>('GET', '/cms/feed-slots'), + feedSlot: (id: string) => + request<{ + id: string + code: string + title: string + slot_key: string + placement: string + active: boolean + system: boolean + updated_at: string + }>('GET', `/cms/feed-slots/${id}`), orders: () => request<{ items: Array<{ diff --git a/apps/admin-h5/src/pages/CMSPage.vue b/apps/admin-h5/src/pages/CMSPage.vue index 5fa8479..de8a86e 100644 --- a/apps/admin-h5/src/pages/CMSPage.vue +++ b/apps/admin-h5/src/pages/CMSPage.vue @@ -3,12 +3,18 @@ import { onMounted, ref } from 'vue' import { adminApi } from '@/api/client' type Banner = Awaited>['items'][number] +type FeedSlot = Awaited>['items'][number] const loading = ref(false) const error = ref('') const items = ref([]) const selected = ref(null) +const slotsLoading = ref(false) +const slotsError = ref('') +const slots = ref([]) +const selectedSlot = ref(null) + async function load() { loading.value = true error.value = '' @@ -22,6 +28,19 @@ async function load() { } } +async function loadSlots() { + slotsLoading.value = true + slotsError.value = '' + try { + const res = await adminApi.feedSlots() + slots.value = res.items || [] + } catch (e) { + slotsError.value = e instanceof Error ? e.message : '加载失败' + } finally { + slotsLoading.value = false + } +} + async function openBanner(id: string) { try { selected.value = await adminApi.banner(id) @@ -30,13 +49,24 @@ async function openBanner(id: string) { } } -onMounted(load) +async function openSlot(id: string) { + try { + selectedSlot.value = await adminApi.feedSlot(id) + } catch { + selectedSlot.value = null + } +} + +onMounted(() => { + void load() + void loadSlots() +})