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:
@@ -133,7 +133,15 @@ CREATE TABLE service_appointments (
|
||||
confirmed_end_at TIMESTAMP WITH TIME ZONE, cancelled_by VARCHAR(36), cancel_reason VARCHAR(500),
|
||||
source VARCHAR(64), request_source VARCHAR(64), created_by_operator_id VARCHAR(36),
|
||||
user_confirmed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
|
||||
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
-- Phase 19:生产 V021 的两条 CHECK 此前在测试库完全缺失 → 非法 status/source
|
||||
-- 在测试期不可见,上到 PostgreSQL 才撞约束返回 500(Phase 11/13 同族约束漂移根因)。
|
||||
CONSTRAINT chk_appointment_status CHECK (status IN (
|
||||
'submitted', 'pending_confirmation', 'confirmed', 'completed', 'cancelled'
|
||||
)),
|
||||
CONSTRAINT chk_source_type CHECK (source IN (
|
||||
'user_self', 'matchmaker_referral', 'activity_followup', 'service_station', 'operator_assisted'
|
||||
))
|
||||
);
|
||||
CREATE TABLE counseling_services (
|
||||
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), service_type VARCHAR(64), request_source VARCHAR(64),
|
||||
@@ -164,11 +172,37 @@ CREATE TABLE counseling_followups (
|
||||
next_followup_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
-- V026: service_programs 专项公益服务项目
|
||||
-- Phase 19:此前仅在 TEST_SCHEMA_OPTIONAL_TABLES 白名单(= 无人测),
|
||||
-- 导致 ServiceProgramService 全链路(创建/编辑/激活/成员审批)零覆盖。已补入并移出白名单。
|
||||
CREATE TABLE service_programs (
|
||||
id VARCHAR(36) PRIMARY KEY, name VARCHAR(128), description CLOB, cover_file_id VARCHAR(36),
|
||||
organizer_org_id VARCHAR(36), start_at TIMESTAMP WITH TIME ZONE, end_at TIMESTAMP WITH TIME ZONE,
|
||||
status VARCHAR(32), created_by VARCHAR(36),
|
||||
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
-- V026 生产约束
|
||||
CONSTRAINT chk_program_status CHECK (status IN ('draft', 'active', 'ended', 'cancelled'))
|
||||
);
|
||||
|
||||
-- V028: service_location_operators 服务站运营人员数据范围(复合主键,无 id 列)
|
||||
-- Phase 19:此前在可选白名单且测试库缺失 → ServiceAppointmentMapper.canAccessByOperator /
|
||||
-- findAccessibleByOperator 的 LEFT JOIN 在测试库直接 BadSqlGrammar,「预约数据范围」路径零覆盖。
|
||||
CREATE TABLE service_location_operators (
|
||||
service_location_id VARCHAR(36) NOT NULL, operator_id VARCHAR(36) NOT NULL, created_by VARCHAR(36),
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
PRIMARY KEY (service_location_id, operator_id)
|
||||
);
|
||||
|
||||
CREATE TABLE service_program_members (
|
||||
id VARCHAR(36) PRIMARY KEY, program_id VARCHAR(36), user_id VARCHAR(36), status VARCHAR(32),
|
||||
applied_at TIMESTAMP WITH TIME ZONE, approved_at TIMESTAMP WITH TIME ZONE, approved_by VARCHAR(36),
|
||||
rejected_reason VARCHAR(500), created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
|
||||
updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
-- 对齐生产 V026:UNIQUE(program_id, user_id)。缺此约束时"被拒/退出后重新申请"
|
||||
-- 撞唯一键 500 的必崩缺陷在测试期完全不可见(Phase 15 同族根因)。
|
||||
CONSTRAINT uk_program_member UNIQUE (program_id, user_id),
|
||||
-- 对齐生产 V026 chk_program_member_status:withdrawn 表示用户退出 / 运营移除
|
||||
CONSTRAINT chk_program_member_status CHECK (status IN ('pending', 'approved', 'rejected', 'withdrawn'))
|
||||
);
|
||||
CREATE TABLE user_local_eligibilities (
|
||||
id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) UNIQUE, eligibility_type VARCHAR(32),
|
||||
@@ -217,7 +251,18 @@ CREATE TABLE home_banners (
|
||||
button_text VARCHAR(64), target_type VARCHAR(64), target_value VARCHAR(200),
|
||||
audience_scope VARCHAR(64), sort_order INT, start_at TIMESTAMP WITH TIME ZONE,
|
||||
end_at TIMESTAMP WITH TIME ZONE, status VARCHAR(32), created_by VARCHAR(36), updated_by VARCHAR(36),
|
||||
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
|
||||
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
-- Phase 19:生产 V019 的三条 CHECK 此前在测试库完全缺失(Phase 11/13 同族约束漂移)。
|
||||
-- target_type 白名单同时是"禁外链"的服务端落点:外链类型不在白名单内。
|
||||
CONSTRAINT chk_banner_target_type CHECK (target_type IN (
|
||||
'activity', 'matchmaker', 'counseling', 'premarital', 'service_location', 'article', 'internal_page', 'none'
|
||||
)),
|
||||
CONSTRAINT chk_banner_audience_scope CHECK (audience_scope IN (
|
||||
'all', 'onboarding', 'searching', 'getting_to_know', 'successful'
|
||||
)),
|
||||
CONSTRAINT chk_banner_status CHECK (status IN (
|
||||
'draft', 'scheduled', 'active', 'inactive', 'expired'
|
||||
))
|
||||
);
|
||||
|
||||
-- V019: service_locations 善缘服务站
|
||||
@@ -227,15 +272,48 @@ CREATE TABLE service_locations (
|
||||
contact_phone_display VARCHAR(32), opening_hours_text CLOB, service_description CLOB,
|
||||
cover_file_id VARCHAR(36), primary_organization_id VARCHAR(36), notice_text CLOB,
|
||||
status VARCHAR(32), created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
-- Phase 19:生产 V019 的服务站状态 CHECK 此前在测试库缺失(同族约束漂移)
|
||||
CONSTRAINT chk_service_location_status CHECK (status IN ('active', 'inactive', 'closed'))
|
||||
);
|
||||
|
||||
-- V042: service_location_announcements 善缘服务站「临时公告」(Phase 19 / U50)
|
||||
-- Phase 19:该表此前**完全缺失于测试库**,而公告是本轮为新链路所建 —— 若不补表,
|
||||
-- 公告 Service 一旦被任何测试调用即 BadSqlGrammar(表不存在),且
|
||||
-- EntityMigrationParityTest 的生产表缺失守卫会直接变红(本轮即由此发现)。
|
||||
-- 对齐 V042:去外键(H2 无 operators 表),保留 chk_announcement_status 白名单
|
||||
-- (「非法状态只在 PostgreSQL 上炸」的必崩盲区同族根因)。
|
||||
CREATE TABLE service_location_announcements (
|
||||
id VARCHAR(36) PRIMARY KEY, service_location_id VARCHAR(36) NOT NULL,
|
||||
title VARCHAR(200) NOT NULL, content CLOB, status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||
published_at TIMESTAMP WITH TIME ZONE, expires_at TIMESTAMP WITH TIME ZONE, sort_order INT NOT NULL DEFAULT 0,
|
||||
created_by VARCHAR(36), updated_by VARCHAR(36),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
CONSTRAINT chk_announcement_status CHECK (status IN ('draft', 'published', 'archived'))
|
||||
);
|
||||
|
||||
-- V026: content_articles 婚恋课堂文章
|
||||
-- V026: content_categories 内容分类(运营后台「内容分类」tab 的唯一落点)
|
||||
-- Phase 19:此前该表完全缺失于测试库,整条分类路径零覆盖(与 Phase 12 缺 user_identities
|
||||
-- 导致首页零覆盖同族)。补表同时补生产 chk_category_status,避免非法状态在测试期不可见。
|
||||
CREATE TABLE content_categories (
|
||||
id VARCHAR(36) PRIMARY KEY, name VARCHAR(128) NOT NULL, code VARCHAR(64) NOT NULL,
|
||||
description CLOB, sort_order INT, status VARCHAR(32),
|
||||
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
-- 对齐生产 V026:UNIQUE(code)。缺此约束时"编码重复"在测试期静默通过,生产 500。
|
||||
CONSTRAINT uk_content_category_code UNIQUE (code),
|
||||
CONSTRAINT chk_category_status CHECK (status IN ('active', 'inactive'))
|
||||
);
|
||||
CREATE TABLE content_articles (
|
||||
id VARCHAR(36) PRIMARY KEY, category_id VARCHAR(36), title VARCHAR(200), summary VARCHAR(500),
|
||||
content CLOB, cover_file_id VARCHAR(36), author_operator_id VARCHAR(36), status VARCHAR(32),
|
||||
publish_at TIMESTAMP WITH TIME ZONE, view_count INT, sort_order INT,
|
||||
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE
|
||||
created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
-- 对齐生产 V026 chk_article_status:此前测试库缺该 CHECK,非法状态只在 PostgreSQL 上炸
|
||||
-- (Phase 11/13/14/15 同族约束漂移根因)。
|
||||
CONSTRAINT chk_article_status CHECK (status IN ('draft', 'scheduled', 'published', 'offline'))
|
||||
);
|
||||
|
||||
-- V010: activities 公益活动(首页近期活动预览)
|
||||
|
||||
Reference in New Issue
Block a user