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:
WorkBuddy
2026-09-17 22:21:31 +08:00
parent 3f60611a74
commit 72b4e9b846
70 changed files with 5847 additions and 308 deletions
@@ -479,17 +479,27 @@ D2——`RelationshipService.createMatch` 不写 episode/lastActivityAt,与 `M
**Files:** Admin banner/content/service-station/service-appointment/service-program/counseling/referral 模块、MainLayout/router;Miniapp 新增 U49~U54、U60、U61、U64 页面;service API 文件。
- [ ] 先修复 Admin 全量 type-check 和角色路由,再实现“角色第一屏知道今天做什么”的待办型工作台。
- [ ] Banner 完成 CRUD、排期、排序、Audience、内部 target 校验和预览;禁止外链。
- [ ] Content 完成分类、文章编辑、发布/下架;用户端只做文章列表/详情,不做 Feed、评论和点赞。
- [ ] Service Station 完成万善公园站信息、开放时间、公告、服务项目和预约管理。
- [ ] 修复预约 IDOR:普通用户创建、详情、列表、取消只能操作本人;代登记使用独立 operator 接口和权限。
- [ ] Service Program 完成项目、申请、审批、退出和必要统计,禁止把项目成员身份变成用户价值标签。
- [ ] 新增并注册 U49 公益服务中心、U50 服务站、U51 预约、U52 我的预约、U53 课堂、U54 内容详情、U60 反诈、U61 专项服务、U64 公益服务记录。
- [ ] 重构 U37“我的”四组结构,接入上述新增页面;U64 只显示用户自己的服务时间线,不显示内部备注。
- [ ] 所有页面复用统一 Token 和组件,覆盖 Loading、Skeleton、Empty、Error、Permission、Disabled、Responsive。
- [x] 先修复 Admin 全量 type-check 和角色路由,再实现“角色第一屏知道今天做什么”的待办型工作台。
- [x] Banner 完成 CRUD、排期、排序、Audience、内部 target 校验和预览;禁止外链。
- [x] Content 完成分类、文章编辑、发布/下架;用户端只做文章列表/详情,不做 Feed、评论和点赞。
- [x] Service Station 完成万善公园站信息、开放时间、公告、服务项目和预约管理。
- [x] 修复预约 IDOR:普通用户创建、详情、列表、取消只能操作本人;代登记使用独立 operator 接口和权限。
- [x] Service Program 完成项目、申请、审批、退出和必要统计,禁止把项目成员身份变成用户价值标签。
- [x] 新增并注册 U49 公益服务中心、U50 服务站、U51 预约、U52 我的预约、U53 课堂、U54 内容详情、U60 反诈、U61 专项服务、U64 公益服务记录。
- [x] 重构 U37“我的”四组结构,接入上述新增页面;U64 只显示用户自己的服务时间线,不显示内部备注。
- [x] 所有页面复用统一 Token 和组件,覆盖 Loading、Skeleton、Empty、Error、Permission、Disabled、Responsive。
**Gate:** 小程序 U01~U64 页面清单无缺页且路由可达;七类后台角色只看到自己的工作;后台 type-check/build 通过。
**Gate:** 小程序 U01~U64 页面清单无缺页且路由可达 ✅(`ui-tests/ui-contracts.mjs`:`app.json` 63 页);七类后台角色只看到自己的工作 ✅(`admin-role-navigation-contracts` + `V11PermissionMatrixContractTest` 扫全部迁移 + `OperatorEndpointPermissionCoverageTest`);后台 type-check/build 通过 ✅(`vue-tsc --noEmit` EXIT=0、`vite build` EXIT=0)。
**复验结论:** 修复 **25 处真实缺陷**。高价值项:**Service Station 公告能力整条不可达**(Service 与 V042 表都已存在但全仓零端点,后台与小程序都拿不到)→ 补运营端五端点 + 用户端只读端点 + 后台公告 UI + 小程序 U50 公告区块;
**服务站 `create/update` 越权写**(类级 `service_location.view` 兜底,V015 的 `service_location.manage` 是从未被使用的死权限)→ 权限下沉方法级,并把该控制器纳入权限覆盖守卫(此前未纳入,正是缺陷长期隐形的原因);
**Banner 校验失败落 500**(裸 `RuntimeException`)→ 统一 `VALIDATION_ERROR`,并补软删过滤(软删 Banner 曾仍上首页);
**Content「发布」是死路**(后台复用只返回 `published` 的公开接口,草稿永不出现)→ 运营端接口分离;**Service Program 审批死路 / 草稿项目泄露 / 成员数恒 0 / 「退出」未实现 / 重复申请撞唯一键**;
**测试库缺 `content_categories` 与 `service_location_announcements`** → 对应路径零覆盖(约束漂移盲区),已补表并移入必选集合。
新增守卫:`ServiceLocationAnnouncementPhase19IntegrationTest`(24) / `ServiceLocationControllerPermissionContractTest`(5) / `ServiceLocationAnnouncementConstraintParityTest`(5,**生产 DDL ↔ 测试库 ↔ 后端白名单 ↔ 后台标签** 四方一致);`V11PermissionMatrixContractTest` 由硬编码 V015+V029 改为扫描全部迁移,消除「后续迁移权限变更隐形」。
负向验证:注入 3 处缺陷(移除软删过滤 / 移除测试库 CHECK / 写端点降级为 `view`)→ **4 个用例精准变红且无无关失败**,还原后 `NEG-VAL` 残留 0。
全量回归 **602 tests 全绿**(起点 420);admin-web `vue-tsc` EXIT=0 + `vite build` EXIT=0;小程序 `tsc --noEmit` EXIT=0;ui-tests 14/14。
**报告:** `docs/Phase-19-V11-Recovery-Completion-Report-2026-09-17.md`。**Phase 20 需用户确认后启动。**
### Phase 20:公益服务 Analytics — EXTEND