fix(ops-backend/service-station): close Phase 19 KEEP-VERIFY — 25 defects (unreachable announcements + station write privilege escalation; banner 500 + soft-delete leak; content publish dead-end; program approval dead-end/draft leak/zero counts/missing withdraw; zero-coverage test schema) ; add V042 announcement chain + 34 guard tests; full regression 602 green
Backend - Service Station: expose the 「临时公告」 chain end-to-end (V042 table/service existed but had ZERO endpoints -> both admin and miniapp could never reach it): 5 operator endpoints (list/create/update/publish/archive) + 1 user-facing read endpoint. - Fix privilege escalation: create/update were guarded only by the class-level service_location.view, making V015's service_location.manage a never-used dead permission -> read-only roles could edit stations. Permissions moved to method level. - Add ServiceLocationOperatorController to OperatorEndpointPermissionCoverageTest (it was NOT scanned, which is why the escalation stayed invisible). - Add create/update validation (address NOT NULL + status CHECK only blew up on PG). - Banner: validation failures threw bare RuntimeException -> 500; now VALIDATION_ERROR -> 400. Add deleted_at IS NULL to both home queries (soft-deleted banners stayed live). - Content: operator list/detail split (admin «publish» was a dead end because it reused the published-only public API); category deactivate/reactivate; article body on edit. - Service Program: operator member list now includes pending (approval reachable); active-only public visibility (draft leak); memberCount/pendingCount aggregation; withdraw/removeMember; reuse row on re-apply (UNIQUE(program_id,user_id) crash). - MeController: add the missing GET /me/service-history route. Schema / guards - schema-h2.sql: add service_location_announcements (+ chk_announcement_status) and content_categories (+ uk_content_category_code, chk_category_status); move both into REQUIRED_IN_TEST_SCHEMA. MigrationValidationTest baseline 41 -> 42. - V11PermissionMatrixContractTest now scans ALL migrations instead of hardcoding V015+V029. - New: ServiceLocationAnnouncementPhase19IntegrationTest (24), ServiceLocationControllerPermissionContractTest (5), ServiceLocationAnnouncementConstraintParityTest (5, prod DDL <-> test schema <-> backend whitelist <-> admin labels). Frontend - admin-web: ServiceStationView gains announcement management (statuses, create/edit, publish/archive with confirm, expiry); write buttons gated by service_location.manage. - miniapp: U50 renders the 临时公告 block via new serviceStationService; page no longer talks to utils/api directly; announcement failure degrades to [] instead of erroring. - ui-tests: U37 four-group + U50 announcement contracts; /home assertion retargeted to the service layer. Verification - Negative verification: 3 injected defects (drop soft-delete filter / drop test-schema CHECK / downgrade write endpoint to view) -> 4 named tests fail precisely, no collateral failures; restored, NEG-VAL residue = 0. - Backend full regression: 602 tests, 0 failures, 0 errors, 3 skipped (baseline 420). - admin-web: vue-tsc --noEmit EXIT=0, vite build EXIT=0. Miniapp: tsc --noEmit EXIT=0. - ui-tests 14/14 PASS.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
-- ============================================================
|
||||
-- V042: Phase 19 —— 善缘服务站「临时公告」
|
||||
--
|
||||
-- 背景:Phase 19 计划要求「Service Station 完成万善公园站信息、开放时间、公告、
|
||||
-- 服务项目和预约管理」,U50 服务站详情页的内容清单也明确包含「临时公告」。
|
||||
-- 但全仓此前**没有任何公告的存储与接口**:既无表、也无实体/Mapper/端点,
|
||||
-- 后台与小程序均无落点 —— 属清单项未实现,而非"已实现待回归"。
|
||||
--
|
||||
-- 设计要点:
|
||||
-- 1. status 用白名单 CHECK 约束(draft/published/archived),
|
||||
-- 避免"非法状态只在生产炸"的漂移盲区(Phase 11/13/14/15 同族根因)。
|
||||
-- 2. expires_at 支撑"临时"语义:用户端查询自动排除已过期公告,
|
||||
-- 运营无需手工下架。NULL 表示长期有效。
|
||||
-- 3. 软删用 deleted_at(与全库一致),不做物理删除,保留追溯。
|
||||
-- 4. 用户端只读 published 且未过期;草稿/归档不得外泄。
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS service_location_announcements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
service_location_id UUID NOT NULL REFERENCES service_locations(id),
|
||||
title VARCHAR(200) NOT NULL,
|
||||
content TEXT,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||
published_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_by UUID REFERENCES operators(id),
|
||||
updated_by UUID REFERENCES operators(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
ALTER TABLE service_location_announcements ADD CONSTRAINT chk_announcement_status
|
||||
CHECK (status IN ('draft', 'published', 'archived'));
|
||||
|
||||
CREATE INDEX idx_announcements_location
|
||||
ON service_location_announcements(service_location_id, status, sort_order)
|
||||
WHERE deleted_at IS NULL;
|
||||
Reference in New Issue
Block a user