fix(organization): 机构三处写操作必崩 500 修复 + type 白名单 + 测试库约束补齐

缺陷本体(实施计划附 B.1,2026-09-19 登记并实测复现,本轮处置):
OrganizationService 的 createOrganization / disableOrganization / enableOrganization
分别写入 Constants.ACCOUNT_STATUS_NORMAL("normal") 与 ACCOUNT_STATUS_SUSPENDED("suspended")。
那是 users.account_status 的取值,而 organizations.status 的 CHECK 白名单
(V001 chk_org_status)只有 ('active','inactive') → 三个写操作全部违约,
生产必崩 PSQLException violates check constraint "chk_org_status" → HTTP 500。
二阶影响:activities.organizer_org_id NOT NULL ⇒ 无机构即无法新建活动,
运营端「建机构 → 建活动 → 报名 → 互选」链路的第一环是断的。

修复:
- Constants 新增 ORG_STATUS_ACTIVE / ORG_STATUS_INACTIVE(独立于 users.account_status,
  附注释说明两者白名单不同,禁止互相复用)
- 三处状态值改为 ORG_STATUS_ACTIVE / ORG_STATUS_INACTIVE
- 新增 OrganizationService.ALLOWED_TYPES + type 白名单前置校验:
  非法值返回 VALIDATION_ERROR(400),不再打到 PostgreSQL 才 500
- 「机构不存在」由裸 RuntimeException(→500 SERVER_ERROR) 改为 NOT_FOUND(404)
- 异常类型统一为 GlobalExceptionHandler.BusinessException —— 独立类
  shared.exception.BusinessException 没有 @ExceptionHandler,抛它仍会退化成 500

根因第三重盲区(缺陷长期隐身的直接原因):
schema-h2.sql 的 organizations 表此前未复刻这两条 CHECK,H2 上写 'normal' 畅通无阻。
已补齐 chk_org_type / chk_org_status 及 NOT NULL / DEFAULT,与生产 DDL 逐值一致。

新增守卫:
- OrganizationConstraintParityTest:生产迁移 DDL ↔ 测试库 schema ↔ 后端白名单 ↔
  Constants 四方比对 + 解析器自检(虚构约束名必须解析失败,防断言静默永真)。
  其中 status DEFAULT 的解析必须先把范围切到 organizations 建表块 —— 全文件搜索会
  误命中 users.account_status(本轮自检脚本实测踩到并修正该缺陷)。
- OrganizationServiceTest:走真实 DB 的 H2 集成测试,9 用例覆盖创建/停用/启用/
  停启往返/六个合法 type/非法 type/名称空白/404 语义,断言落库值而非返回值。

⚠️ 未在本机运行测试:本轮探测确认本机无 Docker、无 JDK,wsl.exe 被安全策略阻止
(与 known-risks.md 的 WSL/Docker 故障一致),mvn -o test 与负向注入均无法执行。
已用等价正则的独立脚本验证守卫判别力与两处 DDL 一致性(PASS,脚本留痕
.workbuddy/_verify_org_guard.py)。
补跑全量回归 + 负向注入前,本项记为「待验证」,不得计入已验收。
This commit is contained in:
WorkBuddy
2026-09-21 20:34:30 +08:00
parent 60c591fc68
commit c195133680
8 changed files with 730 additions and 15 deletions
+1
View File
@@ -63,6 +63,7 @@
| 1.1.0-rc.19 | 2026-09-19 | 批 2/3 收尾完成:E2E 互选 8 + 预约 8 场景真实实现(本机活栈 **65 passed**,负向注入 2 轮)+ INV-011 显式守卫(9 用例);隐私设置接真实落库(V047)+ 5 死开关/2 死配置移除(产品裁定)+ `PUT /feature-flags` 真持久化(V048)+ 5 个无 SQL Mapper 方法补齐;**Task 3.3 四项 + 红娘服务拦截接线**(互选 `expires_at`、随访审计、随访风险检测、Onboarding 确认、注销 blocker 接线 + 竞态兜底);UI 契约门禁 **14→18**(WXML 结构 / WXSS 选择器 / api.js 行为 / miniapp 请求层,并修复小程序 P0 请求层崩溃);全量回归 **712 / 0 / 0 / 3** 全绿(B-9 命令实测) |
| 1.1.0-rc.20 | 2026-09-20 | **数据保留流程落地**(产品 2026-09-19 确认:注销后数据保留不得低于 36 个月):V049 `users.closed_at` + `account.retention.purge` 权限(仅 super_admin)+ `AccountConfig.dataRetentionMonths`(36,配置化)+ `DataRetentionService`(人工审核:到期清单 + 幂等匿名化清除 + 审计;保留期内服务端拒绝)+ `DataRetentionGuardTest` 5 用例(负向注入摘掉下限判断恰 1 红);顺带清偿测试库缺 `user_photos` 表(照片软删路径 BadSqlGrammar 盲区,补表 + 移出 ParityTest 可选白名单)与 MyBatis-Plus `updateById` null 不进 SQL 陷阱(改 `LambdaUpdateWrapper.set(null)`);隐私政策 §3.3 / PIA §8 「30 天删除」修订为 36 个月;迁移基线 48→49。**全量回归 717 / 0 / 0 / 3 全绿**;**E2E 本机模拟 CI 序列 65/65 全绿**(宿主机 Python 3.11);UAT 用例 V1.1 版 46 用例(9 角色 × 7 路径)编写完成 |
| 1.1.0-rc.21 | 2026-09-21 | **V1.0.1 配置与规则纠偏**(根治「技术默认值被误当产品规则 / 配置存在但未闭环 / 文档代码漂移」):① **照片上限配置化** `profile.photo.maxCount`(默认 6 未产品确认)+ `ProfilePolicy` + 服务器端并发强校验(新增 `PHOTO_LIMIT_EXCEEDED` 码,拒绝先于对象存储上传不留孤儿)+ 前端 `mergeAppConfig` 嵌套键修复(服务端值真正下发);② **活体开关闭环** `IdentityPolicy` + `GET /verification/liveness-status` 真实判定点(默认关不阻断实名主流程,开则 `enhancedVerified` 变化,消除假开关)+ 删死 seed `livenessVerification.enabled`;③ **推荐阈值纠偏**(60 为技术默认非产品确认)+ 规则版本化 **V050** `recommendation_records.threshold_snapshot` + 删死重复配置类 `MatchConfig`;④ 新增 `docs/governance/`(config-register / open-questions / decision-register / hardcoded-rule-audit / config-drift-audit / known-risks)并消解 PENDING 阈值/照片矛盾;⑤ 守卫 `PhotoLimitGuardTest` 8 + `IdentityLivenessGuardTest` 5 + `RecommendationThresholdSnapshotGuardTest` 4(均经负向注入可红);⑥ **canonical key 字面化**:`FeatureFlagConfig.KEY_ALIASES`(`identity.liveness.enabled` / 遗留 `livenessVerification.enabled` → 实现字段 `livenessEnabled` 读兼容)+ Controller PUT/DELETE 落库归一 canonical 键 + **V051** 清理遗留键并写入 canonical 行;⑦ **PHOTO-004 真实并发突破实测** `PhotoLimitConcurrencyGuardTest`(3 线程 H2 并发冲顶)——实测咬住「锁照片行+读时计数」在 H2 MVCC 下快照漏读、3/3 全成功突破上限的缺陷,并发控制改为**锁用户行(`UserMapper.lockUserRow` FOR UPDATE)+ 锁后新语句计数**(0 张照片亦有锁可加,补齐「首张并发」缺口),改后恰 1 成功;⑧ `AGENTS.md` 规则 27(配置与规则治理纪律:技术默认值≠产品确认规则 + canonical key 唯一 + 并发上限必须服务器强校验+并发突破测试)。**全量回归 741 / 0 / 0 / 3 全绿(基线 717 + 25 新增 − 1 删死测试);UI 契约门禁 18/18** |
| 1.1.0-rc.22 | 2026-09-21 | **运营端机构管理三处写操作 500 修复**(清偿计划附 B.1 遗留,2026-09-19 登记、本轮处置):`OrganizationService` 创建/停用/启用分别写入 `ACCOUNT_STATUS_NORMAL`("normal") 与 `ACCOUNT_STATUS_SUSPENDED`("suspended") —— 那是 `users.account_status` 的取值,而 `organizations.status` 的 CHECK 白名单(V001 `chk_org_status`)只有 `('active','inactive')` → **三个写操作全部违约必崩**;二阶影响为 `activities.organizer_org_id` NOT NULL ⇒ 无机构即无法建活动(运营端链路级阻塞)。修复:`Constants` 新增 `ORG_STATUS_ACTIVE/INACTIVE`(独立于账号状态);三处改用 `active`/`inactive`;`OrganizationService.ALLOWED_TYPES` 补 `type` 白名单前置校验(非法值 400 取代撞 `chk_org_type` 的 500);「机构不存在」由裸 `RuntimeException`(→500)改为 `NOT_FOUND`(404);异常统一为实际被 `@ExceptionHandler` 捕获的 `GlobalExceptionHandler.BusinessException`(独立类 `shared.exception.BusinessException` **无 handler**,抛它仍会 500 —— 全仓仅 `AnalyticsService` 一处既存误用)。**根因第三重盲区同步补齐**:`schema-h2.sql` 的 `organizations` 此前**未复刻**这两条 CHECK,故 H2 上写 `'normal'` 畅通无阻 —— 已补 CHECK ×2 + NOT NULL + DEFAULT。新增守卫 `OrganizationConstraintParityTest`(生产 DDL ↔ 测试库 ↔ 后端白名单 ↔ Constants 四方比对 + 解析器自检)与 `OrganizationServiceTest`(真实 DB,9 用例)。**⚠️ 未在本机运行测试**:本轮探测确认本机**无 Docker、无 JDK**,`wsl.exe` 被安全策略阻止(同 `known-risks.md` 的 WSL/Docker 故障),`mvn -o test` 与负向注入均无法执行;已用等价正则的独立脚本验证守卫判别力与两处 DDL 一致性(PASS)。**补跑全量回归 + 负向注入前,本项记为「待验证」,不计入已验收。** |
---