Files
danyang-match/docs/Phase-19-V11-Recovery-Completion-Report-2026-09-17.md
T

15 KiB
Raw Blame History

Phase 19 V1.1 Recovery Completion Report — 统一运营后台 + 用户端公益服务页面

日期:2026-09-17 阶段:Phase 19(统一运营后台 ADD + 用户端公益服务页面 REFACTOR) 分支:main | Tag:phase19-recovery-2026-09-17(tag 指向本报告所在提交;主体提交见 git log) Bundle:.workbuddy/backup/phase19-recovery-2026-09-17.bundle(complete history,回读通过) 结论:CLOSED(Gate 全项通过)


一、计划目标与 Gate

官方 Phase 19 计划(WorkBuddy首轮启动提示词 + Phase 01~Phase 26 Jira级任务清单 + 每阶段验收条件.md 第 1734 行起)要求的是「统一运营后台」; STATUS.md 的 V1.1 待实现模块表把 Banner 管理 / 内容管理 / 专项公益服务项目 / 善缘服务站与用户端公益服务页面 一并映射到 Phase 19, 用户端 U 编号页面定义见 docs/善缘丹阳 V1.1 页面级功能清单与交互流程修订版.md(U49~U54 / U60 / U61 / U64 / U37 / U50)。

Gate 要求 结论 证据
Gate ① 小程序页面清单无缺页且路由可达 ✅ 通过 ui-tests/ui-contracts.mjs:app.json 页面数 63;14 个 ui-tests 全绿
Gate ② 七类后台角色只看到自己的工作 ✅ 通过 admin-role-navigation-contracts.mjs 全绿;V11PermissionMatrixContractTest 扫全部迁移;OperatorEndpointPermissionCoverageTest 扩至含服务站
Gate ③ 后台 type-check / build 通过 ✅ 通过 vue-tsc --noEmit EXIT=0;vite build EXIT=0(9.17s,仅 chunk 体积警告)
Gate ④ 公告 / Banner / 内容 / 服务项目 / 预约链路真实可达 ✅ 通过 后端全量回归 602 tests, 0 failures, 0 errors

二、复验方法(KEEP-VERIFY)

本轮不采信任何「历史 ✅」。对每一项交付物按下列顺序逐条核对:

  1. 运行时路径:从 HTTP 端点出发,逐跳确认 Controller → Service → Mapper → DDL 无断点、无死路。
  2. 生产约束:核对生产迁移里的 NOT NULL / CHECK / UNIQUE / 外键,确认代码有前置校验。
  3. 测试库保真度:测试库 schema-h2.sql 是否与生产同约束(缺约束 = 该非法路径零覆盖 = 假绿)。
  4. 守卫有效性:守卫是否解析真实 DDL、是否会精准变红。
  5. 前端落点:后端能力是否有真实前端入口(后台 + 小程序两端)。

三、复验查出的真实缺陷与修复

3.1 死路类(能力写了但接不上 → 用户/运营 100% 用不到)

编号 模块 缺陷 根因 修复
D1 /me GET /me/service-history 端点从未写出 仅有 Service 方法,Controller 缺路由 MeController 补端点,MeRouteContractTest 转绿
D5 Content 后台「发布」是死路:后台复用只返回 published 的公开接口,草稿永远不出现 公开/运营未分离 新增 getOperatorArticles/Categories/ArticleDetail + /content/operator/* 三端点
D10 Service Program 审批死路:运营端名单只返回 approved,pending 申请永不出现,后台「通过/拒绝」按钮永远点不到 名单查询复用用户端语义 findByProgram 全状态 + getProgramMembers
D13 Service Program 「退出」未实现:生产 CHECK 早已允许 withdrawn,无任何代码写入 清单项未实现 withdrawFromProgram + removeMember
D20 Service Station 公告整条不可达:ServiceLocationAnnouncementService 与 V042 表都存在,但全仓零端点,后台与小程序都拿不到 只写了 Service,未接 Controller 运营端 5 端点 + 用户端只读 1 端点

3.2 越权 / 安全类

编号 模块 缺陷 修复
D18 Service Appointment 预约 IDOR:越权访问他人预约 控制器取 principal.id() + service 归属校验 + 运营侧 DataScopeGuard / canAccessByOperator
D21 Service Station 越权写:@PreAuthorize("service_location.view") 挂在类上,create/update 实际只要求"查看"权限;V015 下发的 service_location.manage 成了从未被使用的死权限 → 只读角色可直接改站点 权限下沉到方法级:读 view、写 manage
D22 Service Station ServiceLocationOperatorController 未纳入 OperatorEndpointPermissionCoverageTest 扫描清单 → D21 长期隐形 控制器加入 CONTROLLERS

3.3 约束漂移类(生产有约束、测试库没有 → 只在 PG 上炸)

编号 缺陷 修复
D2 Banner 校验失败抛裸 RuntimeException → 落兜底 handler 变 500(应为 400) 统一 BusinessException(VALIDATION_ERROR)
D3 findActiveBanners / findActiveBannersByScope 漏 deleted_at IS NULL → 软删 Banner 仍上首页 补软删过滤
D9/D23 测试库缺 content_categories、service_location_announcements,且缺 chk_category_status / chk_announcement_status → 对应路径零覆盖 补表 + 补 CHECK;移入 REQUIRED_IN_TEST_SCHEMA
D25 服务站 create/update 无入参校验:address NOT NULL 与 status CHECK 只在 PG 炸 前置校验 → 400
D11 Service Program 公开列表/详情不限定状态 → 草稿项目泄露 getActivePrograms / active 限定
D14 重复申请撞 UNIQUE(program_id, user_id) → 500 复用原记录(被拒/退出后重新申请)

3.4 数据正确性类

编号 缺陷 修复
D12 Service Program 成员数 / 待审数恒为 0(DTO 无字段) DTO 增 memberCount / pendingCount + 聚合统计
D6 Content 分类停用不可恢复(公开接口只给 active) getOperatorCategories + setCategoryStatus(不做物理删除,避免破坏文章外键)
D7/D8 内容编辑看不到正文;前后端字段命名不一致(snake vs camel) 详情带 content;前端对齐 camelCase

3.5 前端与守卫类

编号 缺陷 修复
D4 Banner 后台缺 buttonText / 状态下拉 / 预览 / 禁外链前置校验 BannerManagementView 补齐(预览用模拟小程序卡片,禁外链)
D15 V11PermissionMatrixContractTest 硬编码 V015+V029 → 后续迁移的权限变更全部隐形 改为扫描全部迁移
D16 V042 曾新增 service_appointment.register 权限,与 V029 的 service.assisted_registration 语义重复 删除重复迁移,收敛到既有权限(收敛权限膨胀)
D17 U37「我的」未按信息架构分四组 → 入口找不到归属 重构为四组 + 22 个 bindtap 全部有实现(ui-tests 增断言)
D19 ServiceProgramOperatorPhase19IntegrationTest:114 编译错误(mapper.selectOne 返回实体却赋给 DTO) 修正变量类型;该错误此前让整轮回归卡在 testCompile

四、本轮新增能力(此前完全不存在)

4.1 善缘服务站「临时公告」(V042)

全仓此前无表、无实体、无 Mapper、无接口。本轮补齐:

层 文件 说明
DDL V042__add_service_location_announcements.sql status CHECK(draft/published/archived)、expires_at 支撑"临时"语义、软删、(location_id, status, sort_order) 部分索引
Entity ServiceLocationAnnouncement.java —
Mapper ServiceLocationAnnouncementMapper.java findVisibleByLocation(published + 未过期 + 未软删)、findByLocation(运营全状态)
Service ServiceLocationAnnouncementService.java 可见性 / 白名单 / 标题长度 / ISO-8601 解析 / 404 语义
运营端点 ServiceLocationOperatorController 列表 / 新建 / 编辑 / 发布 / 归档(读 view、写 manage)
用户端点 ServiceLocationController GET /service-locations/{id}/announcements,只读 published + 未过期
后台 UI ServiceStationView.vue 公告抽屉:全状态列表 + 新建/编辑 + 发布/下架二次确认 + 过期时间选择
小程序 U50 service-station + serviceStationService.js 「临时公告」区块,点击弹窗看正文;失败降级为空数组,不阻断站点信息

用户端语义(刻意设计,已在代码注释固化):站点不存在或已软删 → 返回空列表而非错误码,避免存在性探测; 不按 service_locations.status 过滤 —— 站点"休息中/已关闭"时公告(如"本站在改造,请前往相邻服务站")恰恰最需要触达用户。

4.2 用户端公益服务页面(U 编号)

U49~U54 / U60 / U61 / U64 新增并注册;U37「我的」重构为四组结构;U50 补 serviceLocationId 透传与公告区块; U51 补只读「服务站点」字段并回传 serviceLocationId。统一复用 Token / 状态组件(ui-loading / ui-empty / ui-error-state)。


五、守卫与测试保真度(本轮新增)

守卫 用例数 作用
ServiceLocationAnnouncementPhase19IntegrationTest 24 公告真实 DB 路径:草稿不泄露 / 发布可见 / 归档隐藏 / 过期自动隐藏 / 排序 / 软删 / 状态白名单 / 标题校验 / ISO 解析 400 / 404 / 部分更新语义
ServiceLocationControllerPermissionContractTest 5 写端点必须 manage;类级不得挂粗粒度注解(否则新增写方法静默继承读权限);用户端点不得带运营权限
ServiceLocationAnnouncementConstraintParityTest 5 生产 V042 DDL ↔ 测试库 ↔ 后端白名单 ↔ 后台标签 四方一致;表在两侧都存在(自检)
BannerOperatorPhase19IntegrationTest 18 禁外链 / 白名单 / 软删过滤 / 约束保真度
BannerConstraintParityTest 5 Banner 四方一致性
ContentOperatorPhase19IntegrationTest 16 发布死路 / 分类停用 / 校验 / 软删 / 约束保真度
ServiceProgramOperatorPhase19IntegrationTest 29 审批可达 / 草稿泄露 / 退出 / 唯一键 / 统计 / 约束保真度
ServiceProgramControllerPermissionContractTest 4 写 manage、读 view、用户端点不守护
OperatorEndpointPermissionCoverageTest 2 扫描清单新增服务站运营端
MeRouteContractTest / ServiceAppointmentOwnerGuardTest — /me/service-history 路由;预约归属与数据范围

AppealServiceTest 式的 assertTrue(true) 假绿在本轮被清除;EntityMigrationParityTest 的 testSchemaMissingTablesAreWhitelisted 主动报出了 D23(生产表在测试库缺失),证明该守卫解析的是真实迁移 DDL。


六、负向验证(证明守卫是承重的,而非装饰)

一次性注入 3 处缺陷(均以 NEG-VAL 标记),跑定向回归 → 4 个用例精准变红,且无任何无关失败:

注入缺陷 精准变红的用例 断言信息
移除 findVisibleByLocation 的 deleted_at IS NULL softDeletedAnnouncementIsInvisibleToBothSides 软删后用户端不得再看到该公告
从测试库移除 chk_announcement_status databaseCheckRejectsIllegalStatusEvenWhenServiceValidationIsBypassed 期望抛异常但未抛(非法状态落库成功)
同上 productionAndTestSchemaDeclareIdenticalAnnouncementStatusCheck 未能解析出 chk_announcement_status(守卫显式报错而非假绿)
把 update 的权限降级为 service_location.view everyWriteEndpointRequiresManagePermission update 权限码不符合 V015(期望 manage,实为 view)

还原后 grep -rc "NEG-VAL" = 0,全量回归恢复全绿。


七、回归证据

后端全量:Tests run: 602, Failures: 0, Errors: 0, Skipped: 3 / BUILD SUCCESS
  (复验起点 420 → 本轮 602,净 +182;本轮相对「修完编译错误」基线 568 净 +34)
前端 admin-web:vue-tsc --noEmit EXIT=0;vite build EXIT=0(9.17s,仅 chunk 体积警告)
小程序:tsc --noEmit EXIT=0
契约:ui-tests 14/14 PASS(含「63 页清单」「U37 四组」「U50 公告」断言)

八、明确记录的设计决策(非缺陷,但需知情)

  1. service_location.manage 仅授予 platform_admin,未扩权给 service_station_operator (V015 该角色刻意只有 view,定位是"现场处理预约")。 因此服务站信息与公告的维护入口对平台管理员开放;服务站运营人员在后台可见站点与公告列表(只读), 写按钮按同一权限显隐,避免"点了才 403"。 若后续要求服务站自主维护本站公告,需单独评审并同时引入 service_location_operators 的站点级数据范围校验(不能只加权限 —— 那会让任一服务站运营改到别人的站点)。
  2. /service-locations/** 不做匿名放行:公告是登录后可见内容,鉴权走 SecurityConfig 的 /** → authenticated 兜底, 与 /home 一致。
  3. 公告不做物理删除:下架走 archived,保留可追溯性;软删字段仍保留但不由运营直接触发。
  4. /home 聚合刻意不塞公告:公告是"站点详情"维度内容,塞进首页聚合会让首页随公告变化而频繁失效缓存。

九、遗留与下一阶段

项 状态
ServiceLocationOperatorController 支持 service_station_operator 站点级自维护 ⏳ 需产品确认(见决策 1)
公告富文本 / 图片 ⏳ 当前为纯文本 content
计划表中「本地资格声明与审核」(Phase 04~05)等仍有 ⏳ 项 后续阶段
下一阶段 Phase 20 起(等用户确认)

附:本轮改动文件(节选)

后端 service_appointment/controller/ServiceLocationOperatorController.java(重写)、 service_appointment/controller/ServiceLocationController.java(新增)、 service_appointment/service/ServiceLocationAnnouncementService.java、 service_appointment/mapper/ServiceLocationAnnouncementMapper.java、 service_appointment/entity/ServiceLocationAnnouncement.java、 db/migration/V042__add_service_location_announcements.sql、 banner/controller/HomeBannerOperatorController.java、banner/mapper/HomeBannerMapper.java、 content/service/ContentService.java、content/controller/ContentController.java、 service_program/**、me/controller/MeController.java

前端 admin-web/src/modules/service-station/ServiceStationView.vue、 admin-web/src/modules/banner/BannerManagementView.vue、 admin-web/src/modules/content/ContentManagementView.vue、 admin-web/src/modules/service-program/ServiceProgramView.vue、 miniapp/pages/service-station/*、miniapp/services/serviceStationService.js、 miniapp/pages/mine/*、miniapp/pages/service-appointment/*

守卫 / 测试 backend/src/test/resources/schema-h2.sql、 ServiceLocationAnnouncementPhase19IntegrationTest、 ServiceLocationControllerPermissionContractTest、 ServiceLocationAnnouncementConstraintParityTest、 BannerOperatorPhase19IntegrationTest、BannerConstraintParityTest、 ContentOperatorPhase19IntegrationTest、ServiceProgramOperatorPhase19IntegrationTest、 OperatorEndpointPermissionCoverageTest、EntityMigrationParityTest、MigrationValidationTest、 ui-tests/miniapp-reference-contracts.mjs