diff --git a/backend/src/main/java/com/danyang/match/platform/organization/controller/OrganizationController.java b/backend/src/main/java/com/danyang/match/platform/organization/controller/OrganizationController.java index a762e60..7b292aa 100644 --- a/backend/src/main/java/com/danyang/match/platform/organization/controller/OrganizationController.java +++ b/backend/src/main/java/com/danyang/match/platform/organization/controller/OrganizationController.java @@ -35,7 +35,7 @@ public class OrganizationController { @PostMapping public ApiResponse create(@AuthenticationPrincipal OperatorPrincipal principal, @RequestBody Map params) { - return ApiResponse.ok(organizationService.createOrganization(params)); + return ApiResponse.ok(organizationService.createOrganization(params, principal.id())); } @PutMapping("/{id}") diff --git a/backend/src/main/java/com/danyang/match/platform/organization/service/OrganizationService.java b/backend/src/main/java/com/danyang/match/platform/organization/service/OrganizationService.java index 4ec6f39..cb4cdb2 100644 --- a/backend/src/main/java/com/danyang/match/platform/organization/service/OrganizationService.java +++ b/backend/src/main/java/com/danyang/match/platform/organization/service/OrganizationService.java @@ -78,8 +78,16 @@ public class OrganizationService { return organizationMapper.selectById(id); } + /** + * 创建机构。 + * + *

⚠️ {@code operatorId} 必须由 Controller 从认证主体透传:本方法会写 + * {@code CREATE_ORGANIZATION} 审计行,缺失操作人即审计不可归属。 + * 2026-09-21 活栈验收实测发现该参数原先未透传 —— 7 条 CREATE 审计的 + * {@code operator_id} 全为 NULL,而停用 / 启用却正确记录,属「同一族缺陷的创建分支」。 + */ @Transactional - public Organization createOrganization(Map params) { + public Organization createOrganization(Map params, String operatorId) { String name = asNonBlankString(params.get("name")); if (name == null) { throw new BusinessException(ApiResponseCode.VALIDATION_ERROR, "机构名称不能为空"); @@ -93,7 +101,7 @@ public class OrganizationService { org.setContactName(asNonBlankString(params.get("contactName"))); org.setStatus(Constants.ORG_STATUS_ACTIVE); organizationMapper.insert(org); - saveAuditLog(null, "CREATE_ORGANIZATION", "organization", org.getId(), null, null); + saveAuditLog(operatorId, "CREATE_ORGANIZATION", "organization", org.getId(), null, null); return org; } diff --git a/backend/src/test/java/com/danyang/match/platform/organization/OrganizationServiceTest.java b/backend/src/test/java/com/danyang/match/platform/organization/OrganizationServiceTest.java index 062d927..3dd5a83 100644 --- a/backend/src/test/java/com/danyang/match/platform/organization/OrganizationServiceTest.java +++ b/backend/src/test/java/com/danyang/match/platform/organization/OrganizationServiceTest.java @@ -1,8 +1,11 @@ package com.danyang.match.platform.organization; import com.danyang.match.platform.organization.entity.Organization; +import com.danyang.match.platform.audit.entity.AuditLog; +import com.danyang.match.platform.audit.mapper.AuditLogMapper; import com.danyang.match.platform.organization.mapper.OrganizationMapper; import com.danyang.match.platform.organization.service.OrganizationService; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.danyang.match.platform.shared.constant.Constants; import com.danyang.match.platform.shared.enums.ApiResponseCode; import com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException; @@ -14,6 +17,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; +import java.util.List; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -45,6 +49,10 @@ class OrganizationServiceTest { @Autowired private OrganizationService organizationService; @Autowired private OrganizationMapper organizationMapper; + @Autowired private AuditLogMapper auditLogMapper; + + /** 审计归属断言用的操作人 id(模拟已认证的运营主体) */ + private static final String OPERATOR_ID = "op-audit-e2e"; private static Map params(String name, String type) { Map params = new HashMap<>(); @@ -54,13 +62,29 @@ class OrganizationServiceTest { } private Organization createAndReload(String name, String type) { - Organization created = organizationService.createOrganization(params(name, type)); + Organization created = organizationService.createOrganization(params(name, type), OPERATOR_ID); assertNotNull(created.getId(), "机构应被分配 id(AutoFillHandler.insertFill)"); Organization reloaded = organizationMapper.selectById(created.getId()); assertNotNull(reloaded, "机构应真实落库"); return reloaded; } + // ==================== 审计归属 ==================== + + @Test + @DisplayName("创建机构:审计日志必须记录操作人(2026-09-21 活栈实测 7/7 条 CREATE 审计 operator_id 为 NULL)") + void createOrganizationRecordsOperatorInAuditLog() { + Organization org = createAndReload("审计归属机构-" + System.nanoTime(), "other"); + + List logs = auditLogMapper.selectList(new LambdaQueryWrapper() + .eq(AuditLog::getResourceId, org.getId()) + .eq(AuditLog::getAction, "CREATE_ORGANIZATION")); + + assertEquals(1, logs.size(), "创建机构应恰好留下 1 条 CREATE_ORGANIZATION 审计"); + assertEquals(OPERATOR_ID, logs.get(0).getOperatorId(), + "审计行必须记录操作人 —— operator_id 为 NULL 的审计不可归属,等于没有审计"); + } + // ==================== 三个写操作的落库状态 ==================== @Test @@ -118,7 +142,7 @@ class OrganizationServiceTest { @DisplayName("创建机构:非法 type 前置拒绝为业务异常(原实现不校验,会撞 chk_org_type 变 500)") void createOrganizationRejectsIllegalType() { BusinessException ex = assertThrows(BusinessException.class, - () -> organizationService.createOrganization(params("非法类型机构", "sponsor")), + () -> organizationService.createOrganization(params("非法类型机构", "sponsor"), OPERATOR_ID), "非法 type 必须被前置拒绝为 400 业务错误,而不是打到 PostgreSQL 才 500"); assertEquals(ApiResponseCode.VALIDATION_ERROR, ex.getCode()); @@ -144,9 +168,9 @@ class OrganizationServiceTest { @DisplayName("创建机构:名称为空或纯空白被拒") void createOrganizationRejectsBlankName() { assertThrows(BusinessException.class, - () -> organizationService.createOrganization(params(null, "other"))); + () -> organizationService.createOrganization(params(null, "other"), OPERATOR_ID)); assertThrows(BusinessException.class, - () -> organizationService.createOrganization(params(" ", "other"))); + () -> organizationService.createOrganization(params(" ", "other"), OPERATOR_ID)); } @Test diff --git a/docs/PROGRESS-ASSESSMENT-2026-09-21.md b/docs/PROGRESS-ASSESSMENT-2026-09-21.md index 843c7cf..8ce36cc 100644 --- a/docs/PROGRESS-ASSESSMENT-2026-09-21.md +++ b/docs/PROGRESS-ASSESSMENT-2026-09-21.md @@ -55,16 +55,28 @@ ## 三、关键发现:一处 High 缺陷登记后未修复 -### 3.1 机构创建 / 停用 / 启用三个写操作必然失败(High,**2026-09-21 已修复,待补跑回归验证**) +### 3.1 机构创建 / 停用 / 启用三个写操作必然失败(High,**2026-09-22 已验证关闭**) -> **处置更新(2026-09-21 当天)**:本项已按第五节 P0-1 执行完毕 —— +> **处置完成(2026-09-22 复核)**:本项已按第五节 P0-1 执行完毕 —— > `Constants` 新增 `ORG_STATUS_ACTIVE/INACTIVE`、三处状态值修正、`type` 白名单前置校验、 > 测试库 `schema-h2.sql` 补两条 CHECK 及 NOT NULL/DEFAULT、 -> 新增 `OrganizationConstraintParityTest` 与 `OrganizationServiceTest`(9 用例)。 -> **但本机无 Docker / 无 JDK(`wsl.exe` 被安全策略阻止),`mvn -o test` 与负向注入未能执行**, -> 故当前状态为「**已修复待验证**」而非「已验证」。 -> 详见 `docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md` 附 B.1 的处置留痕 -> 与 `docs/STATUS.md` 版本行 rc.22。 +> 新增 `OrganizationConstraintParityTest` 与 `OrganizationServiceTest`。 +> +> **2026-09-22 完成全部补跑验证(此前记为「已修复待验证」的缺口已补齐)**: +> 1. **全量回归 757 / 0 / 0 / 3,BUILD SUCCESS**(108 个测试类,6:44;基线 741 + 新增 16 = 757); +> 2. **负向注入可红**:把 `Constants.ORG_STATUS_ACTIVE` 改回 `"normal"` 后,16 个用例变 +> **3 失败 + 7 错误** —— 守卫 `backendWhitelistsMatchProductionDdl` / +> `orgStatusConstantsCoverProductionWhitelist` / `orgStatusMustNotReuseAccountStatusValues` +> 三条断言各自命中该误用,服务侧 7 例被 H2 `CHK_ORG_STATUS` 拦下。注入采用 +> **仓库只读挂载 + 容器内副本改写**,工作区哈希前后一致(`28aaaedb…`),源码零污染; +> 3. **活栈实测(Docker 全栈)**:创建 / 停用 / 启用**全部 2xx** 且落库为 `active`/`inactive`, +> 非法 `type` 与缺字段 400 `VALIDATION_ERROR`,不存在机构 404 `NOT_FOUND`, +> **全流程零 5xx**(15/15 通过); +> 4. **对已部署栈跑 `pytest tests/e2e`:65 passed**。 +> +> **本项状态:已验证关闭。** +> 另:活栈验收中新发现同族缺陷「创建机构审计缺操作人」——`CREATE_ORGANIZATION` 审计行 +> `operator_id` 7/7 为 NULL(停用/启用却正确记录),已一并修复并复验,见 `docs/STATUS.md` rc.23。 该问题于 2026-09-19 已在实施计划「附 B.1」中登记并实测复现,但截至本次评估(2026-09-21)**代码未改动**。 @@ -156,10 +168,11 @@ ### 优先级 P0 — 建议立即做(无需用户决策,纯技术清偿) -1. **修复机构三个写操作 + 补测试与守卫**(3.1)。✅ **代码已完成(2026-09-21)**; - ⚠️ 验收项中的「全量回归 + 负向注入 + 活栈实测」因本机无 Docker/JDK **未执行**,待在有该环境处补跑。 - 原验收标准:`mvn -o test` 全绿 + 新增用例经负向注入可变红 + 活栈实测 - `POST /operator/organizations` 返回 2xx。 +1. **修复机构三个写操作 + 补测试与守卫**(3.1)。✅ **已修复且已通过全部验收项(2026-09-22)**: + 全量回归 **757 / 0 / 0 / 3 全绿**;负向注入 16 例变 **3 失败 + 7 错误**(守卫三条断言全中 + H2 `CHK_ORG_STATUS` 拦下 7 例); + 活栈实测 `POST /operator/organizations` 与停用/启用**均 2xx**、非法输入 400、不存在 404、全程零 5xx(15/15); + 对已部署栈 `pytest tests/e2e` **65 passed**。**本项关闭。** + 附带清偿同族缺陷「创建机构审计缺操作人」(见 rc.23)。 2. **确认 Git 可用性**(4.1)。决定是否加 `safe.directory`,否则先不要写代码 —— 写了也打不了点。 ### 优先级 P1 — 需用户/环境配合 diff --git a/docs/STATUS.md b/docs/STATUS.md index 5437ee9..b0bbf62 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -63,7 +63,8 @@ | 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)。**补跑全量回归 + 负向注入前,本项记为「待验证」,不计入已验收。** | +| 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,10 用例)。**✅ 2026-09-22 补跑验证全部通过(此前记为「待验证」)**:全量回归 **757 / 0 / 0 / 3 全绿**(BUILD SUCCESS,108 个测试类,6:44;基线 741 + 新增 16 = 757);负向注入 16 例变 **3 失败 + 7 错误**(守卫三条断言全中 + H2 `CHK_ORG_STATUS` 拦下 7 例;注入采用仓库只读挂载 + 容器内副本改写,源码零污染);活栈实测创建/停用/启用 **均 2xx**、非法 type 与缺字段 400、不存在 404、**全程零 5xx**(15/15);对已部署栈 `pytest tests/e2e` **65 passed**。**本项已验证关闭。** | +| 1.1.0-rc.23 | 2026-09-22 | **机构审计归属修复 + 测试环境 Docker 化部署与全链验证**。① **活栈验收中新发现并修复**:复核 `audit_logs` 见 `CREATE_ORGANIZATION` 的 `operator_id` **7/7 为 NULL**(而 `DISABLE/ENABLE_ORGANIZATION` 均正确记录)—— 控制器持有 `@AuthenticationPrincipal` 却未向 Service 透传,**审计不可归属等于没有审计**;`createOrganization(params, operatorId)` 改为透传 `principal.id()`,并新增断言 `createOrganizationRecordsOperatorInAuditLog` 长期守卫。修后复验:新写入的 7 条 CREATE 审计全带操作人,旧 14 条仍为 NULL(前后对照清晰)。② **Docker 测试环境部署成功**:`docker compose -p danyang` 起全栈 6 容器(postgres 5434 / redis 6381 / minio 9003-9004 / backend 8085 / admin-web / nginx 6688),51 个 Flyway 迁移全部成功、`GET /admin/` 200、`pytest tests/e2e` **65 passed**。③ **测试工具链重建并固化为仓库资产**:测试镜像 `danyang-match-backend-test` 与命名卷 `danyang-m2-cache` 因 Docker Desktop 重装一并丢失 → 构建定义落盘 `infrastructure/test-toolchain/Dockerfile`(此前它是临时产物所以才会丢);卷由 108.5MB **不完整快照**补全(原缺 `spring-boot-starter-parent`,故离线跑必失败)。④ **环境坑(已同步 known-risks)**:Docker Hub 域名 **DNS 被投毒**(`registry-1.docker.io`→31.13.95.37、`auth.docker.io`→162.125.32.15,均非真实 IP)→ 改由 `docker.m.daocloud.io` 拉取后 retag;Maven 走阿里云(实测 5337 vs 685 KB/s,快 8 倍);compose 项目名原用 `infra`,与另一项目 `AI智习室`(workdir `E:\develop\AI智习室\ai-study-room\infra`)**撞名**,其 `--remove-orphans` 会把本项目的容器当孤儿删掉 → 改用 `-p danyang`。⑤ **UI 门禁脆弱性修复**:第 16 项 `ui-document-baseline-contracts.mjs` 报假红(`未能解析出端口映射(守卫自身失效)`),真因是 `docker-compose.staging.yml` 是 **CRLF** 而守卫正则要求 `\n` 紧随 `container_name` 之后(`prod.yml` 是 LF 所以此前能过)→ 解析改为行尾无关,真实断言未削弱。**全量回归 757 / 0 / 0 / 3 全绿;UI 契约门禁 18/18;E2E 65/65**。验证脚本留痕 `.workbuddy/run-regression.sh`、`negative-injection.sh`、`org-live-acceptance.py` | --- diff --git a/docs/governance/known-risks.md b/docs/governance/known-risks.md index c31bc8e..7545131 100644 --- a/docs/governance/known-risks.md +++ b/docs/governance/known-risks.md @@ -1,20 +1,22 @@ # 已知风险 — known-risks.md -> **版本**:V1.0.1 配置与规则纠偏基线 · **生效**:2026-09-21 · **owner**:WorkBuddy +> **版本**:V1.0.1 配置与规则纠偏基线 · **生效**:2026-09-21 · **最后更新**:2026-09-22 · **owner**:WorkBuddy > 登记「已知但本轮不处置」的风险与债务。每项注明触发条件、影响面、处置去向。 --- | # | 风险 / 债务 | 影响面 | 处置去向 | |---|------------|--------|----------| -| KR-01 | **minio pin tag 拉取失败**:`minio/minio:RELEASE.2025-09-07T16-13-09Z` 在本机拉取被拒(疑似上游清理或代理限制);E2E 序列用临时 backend 镜像绕过,S3 用例未触 | 本机 E2E 全栈序列;生产部署镜像拉取 | 换 pin 版本或走可拉取的镜像源;上 CI 后实测。生产 compose 未改。 | +| KR-01 | **minio pin tag 拉取失败(2026-09-22 复测:仍未解决)**:`minio/minio:RELEASE.2025-09-07T16-13-09Z` 在本机从 Docker Hub 与 5 个国内镜像源(daocloud / 1panel / rat.dev / nju / sjtug / 1ms.run)**全部取不到该 tag**。本机 E2E 以「把本地 `quay.io/minio/minio:latest` 重打为钉版 tag」绕过 —— ⚠️ **这是对钉版基线的偏离**,仅限本地测试环境(机构接口验收不依赖对象存储);CI(ubuntu runner)可达 Docker Hub,不受影响。 | 本机 E2E 全栈序列;生产部署镜像拉取 | 换 pin 版本或走可拉取的镜像源;上 CI 后实测。生产 compose 未改。 | | KR-02 | **`system_configs` 死表**:V001 建表 + seed(match_rule_version/daily_recommendation_limit/match_score_threshold='60'/recommendation_cooldown_days)+ demo-seed 一批 flag 行,**0 个 Java 读取点**;与活键 `match.*` / `features.*` 各写一份(seed 与代码漂移) | 配置可信源混淆(运维改 system_configs 不生效) | 未来迁移裁定:整体退役(DROP 需专门迁移)或接成真实配置源。见 config-drift-audit §一。 | | KR-03 | **`match_rule_configs` 死表**(V006,含 10 维权重 + minimum_score)+ `MatchRuleConfigMapper.findActive/findByVersion` 无调用方 | 规则版本化走轻量路线(rule_version + V050 threshold_snapshot),此表闲置 | 未来项:接成「规则版本唯一读取源」或删表。见 config-drift-audit §二。 | | KR-04 | **打分占位未产品化**:`calculateScore` 恒 50.0、互选公式 0.6/0.4、level 分档 80/60 均硬编码(V1 占位) | 推荐分数无业务含义,不得对外包装「匹配成功率」 | 正式化时随 10 维权重一起配置化 + 记入规则版本(open-questions OQ-MATCH-001 关联项)。 | | KR-05 | **本地资格佐证到期清理作业未实现**:`features.localEvidenceRetentionDays=365` 仅配置,无 @Scheduled 清理任务(文件本体交对象存储生命周期规则兜底) | 佐证文件可能超期留存 | Phase 22 运维任务(Phase-04 报告 :46 登记)。 | -| KR-06 | **GitHub Actions 实测未跑**:E2E 65/65 与回归 733/740 均为本机实测;CI 流水线未推过一次绿 | 「CI 绿」仍依赖一次推送 | 批 3 遗留项(Phase 24 启动前);一次 push 触发。 | -| KR-07 | **本机 WSL/Docker 环境故障(2026-09-21 发现)**:`LxssManager` 服务缺失、`wsl -l` 报「Linux 子系统未安装」,Docker Desktop(WSL2 引擎)无法启动引擎;需管理员权限修复(`wsl --install` / DISM 虚拟机平台) | 本机 docker 容器跑 maven 的路径暂时不可用(已改用宿主机 JDK+Maven 兜底);E2E 活栈序列需 Docker 恢复后重跑 | 用户侧以管理员身份修复 WSL;修复后验证 `danyang-match-backend-test` 镜像仍可用(数据根 `D:\Data\Docker-Windows`)。 | +| KR-06 | **GitHub Actions 实测未跑**:E2E 65/65 与回归 757 / 0 / 0 / 3 均为本机实测(2026-09-22 复跑);CI 流水线仍未推过一次绿 | 「CI 绿」仍依赖一次推送。注意本机与 runner 的差异:**E2E 作业的 docker CLI 必须在 PATH 上** —— 本机实测该点会让 16 个依赖 `docker exec` 取库内 fixture 的用例报 `FileNotFoundError`(不是产品缺陷) | 批 3 遗留项(Phase 24 启动前);一次 push 触发。 | +| KR-07 | ~~**本机 WSL/Docker 环境故障(2026-09-21)**~~ → **2026-09-22 已解除**:Docker Desktop 由用户启动后引擎正常(Server 29.8.0 / linux / 16 CPU / 8GB;CLI 位于 `D:\Data\Docker\DockerDesktop\resources\bin\docker.exe`,**不在 PATH**)。`wsl.exe` 仍被安全策略阻止(Program Blacklist,不可审批绕过),但本机容器路径已不依赖 WSL。 | 原生 docker 路径可用;本机全栈部署与 E2E 65/65 均已实跑 | 无(已闭环)。后续在本机跑容器需用 CLI 绝对路径,或自行把该 bin 加入 PATH。 | +| KR-08 | **Docker Hub 域名 DNS 被投毒(本机网络,2026-09-22 实测)**:`registry-1.docker.io` → `31.13.95.37`(Facebook 段)、`auth.docker.io` → `162.125.32.15`(Dropbox 段),**均为伪 IP** → `docker pull` 一律超时。宿主机对这两个域名的直连探测有正常响应(401/405),说明是**解析层**被污染而非出口被封;Maven Central 与 apk 源均正常可达。 | 本机所有需要拉取 Docker Hub 镜像的构建(temurin / node / nginx / postgres / redis / minio) | 绕行:从 `docker.m.daocloud.io` 等国内镜像源拉取后 retag 成本地名(本轮已实测可行);Maven 侧改用阿里云源(实测 5337 vs 685 KB/s)。**未改 daemon DNS**,避免影响用户其他网络行为。 | +| KR-09 | **compose 项目名撞名(本机,2026-09-22 实测已造成实际破坏)**:本机另一项目 `AI智习室` 位于 `E:\develop\AI智习室\ai-study-room\infra`,**同样以 `-p infra` 为项目名**;其重新部署带 `--remove-orphans` 时,会把本项目以 `infra` 为项目名起的容器当作孤儿容器**删除**(本轮已实测发生:`danyang-match-postgres` / `-redis` 被清)。已确认邻项目 Postgres 卷未被重新初始化、**数据无丢失**。 | 本机多项目共用 `infra` 项目名时的容器互删 | 本项目本地栈统一改用 `docker compose -p danyang`;如需彻底隔离,各项目应使用互不相同的项目名。 | --- -*最后更新: 2026-09-21 — 登记 7 项已知风险(minio / system_configs 死表 / match_rule_configs 死表 / 打分占位 / 佐证清理作业 / CI 实测 / 本机 WSL 故障)。* +*最后更新: 2026-09-22 — KR-07 解除(Docker 已恢复);新增 KR-08(Docker Hub DNS 投毒)与 KR-09(compose 项目名撞名);KR-01 / KR-06 按复测结果订正。* diff --git a/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md b/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md index 78363bc..0185439 100644 --- a/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md +++ b/docs/superpowers/plans/2026-09-18-phase23-closeout-and-uat-readiness.md @@ -523,11 +523,22 @@ > - **新增守卫**:`OrganizationConstraintParityTest`(生产 DDL ↔ 测试库 ↔ 后端白名单 ↔ > Constants 四方比对 + 解析器自检)、`OrganizationServiceTest`(走真实 DB, > 9 个用例覆盖创建/停用/启用/往返/type 合法与非法/名称空白/404 语义)。 - > - ⚠️ **未在本机运行测试**:2026-09-21 探测确认本机**无 Docker、无 JDK**, - > `wsl.exe` 被安全策略阻止(与 `known-risks.md` 的 WSL/Docker 故障一致), - > 故 `mvn -o test` 与负向注入**均未能执行**。已用等价正则的独立脚本验证 - > 守卫机制判别力与两处 DDL 一致性(PASS,脚本留痕于 `.workbuddy/_verify_org_guard.py`)。 - > **补跑全量回归 + 负向注入前,本项不得计入「已验证」。** + > - ✅ **2026-09-22 补跑验证完成(此前记为「待验证」,现关闭)**: + > ① **全量回归 757 / 0 / 0 / 3,BUILD SUCCESS**(108 个测试类,6:44;基线 741 + 新增 16 = 757); + > ② **负向注入可红**:把 `ORG_STATUS_ACTIVE` 改回 `"normal"` → 16 例变 **3 失败 + 7 错误**, + > 守卫三条断言(后端白名单 vs DDL / Constants vs 白名单 / 机构状态 vs 账号状态不相交)各自命中, + > 服务侧 7 例由 H2 `CHK_ORG_STATUS` 拦下。注入用**仓库只读挂载 + 容器内副本改写**, + > 工作区哈希前后一致,源码零污染(脚本 `.workbuddy/negative-injection.sh`); + > ③ **活栈实测**创建/停用/启用**全部 2xx**、非法 type 与缺字段 400、不存在 404、**零 5xx**(15/15); + > ④ 对已部署 Docker 栈 `pytest tests/e2e` → **65 passed**。 + > - **2026-09-22 附带清偿同族缺陷:创建机构审计缺操作人**。活栈验收复核 `audit_logs` 发现 + > `CREATE_ORGANIZATION` 的 `operator_id` **7/7 为 NULL**(而 `DISABLE/ENABLE_ORGANIZATION` + > 都正确记录了操作人)—— 控制器持有 `@AuthenticationPrincipal` 却未向 Service 透传, + > 审计不可归属等于无效审计。已改为 `createOrganization(params, principal.id())`, + > 并新增断言 `createOrganizationRecordsOperatorInAuditLog` 长期守卫。 + > 修后复验:新写入的 7 条 CREATE 审计全部带 `operator_id`,旧 14 条仍为 NULL(前后对照清晰)。 + > - ⚠️ **仍未处置(不在本轮范围)**:`AnalyticsService` 一处误用独立类 + > `shared.exception.BusinessException`(该类无 `@ExceptionHandler`,抛出会退化为 500)。 2. **互选记录永不过期**(Medium,即批 3 Task 3.3 的 P16-T13) `createMutualConsent` 从不写 `expires_at`(列无默认值),而 `confirmSideIfPending` 的过期闸门 `expires_at IS NULL OR expires_at > NOW()` diff --git a/infrastructure/test-toolchain/Dockerfile b/infrastructure/test-toolchain/Dockerfile new file mode 100644 index 0000000..9f84f0b --- /dev/null +++ b/infrastructure/test-toolchain/Dockerfile @@ -0,0 +1,16 @@ +# 后端测试工具链镜像 —— 本机无 JDK / 无 Maven,Java 测试一律经此容器执行。 +# +# 构建: +# docker build -t danyang-match-backend-test:latest infrastructure/test-toolchain +# +# 运行(两处必挂,漏任一处都会导出完全错误的结论,见 2026-09-18 实施计划 附 B-9): +# -v "":/proj ← 必须挂【仓库根】而非 backend/,否则 +# MigrationValidationTest 等因相对仓库根读文件而集体 NoSuchFile +# -v danyang-m2-cache:/root/.m2 ← 必须挂命名卷。空卷会遮蔽镜像内置 /root/.m2, +# 首次需先由 .workbuddy/m2-cache 灌注 +# +# 说明:2026-09-21 本镜像与 danyang-m2-cache 卷曾因 Docker Desktop 重装一并丢失, +# 故由 .workbuddy/backend-test-dockerfile.txt 固化为仓库内可复现定义。 +FROM eclipse-temurin:21-jdk-alpine +RUN apk add --no-cache maven +WORKDIR /proj/backend diff --git a/ui-tests/ui-document-baseline-contracts.mjs b/ui-tests/ui-document-baseline-contracts.mjs index c7a916b..4a11a8b 100644 --- a/ui-tests/ui-document-baseline-contracts.mjs +++ b/ui-tests/ui-document-baseline-contracts.mjs @@ -111,7 +111,7 @@ if (!existsSync(resolve(root, prodComposePath))) { // 不直接写 /ports:\s*\n\s*-\s*"/ —— 端口上方允许有注释行,那样会匹配不到并让守卫误判为失效。 const prodCompose = read(prodComposePath) const prodAdminBlock = prodCompose.match( - /container_name:\s*danyang-match-admin\n([\s\S]*?)(?=\n {2}[a-z]|$)/, + /container_name:\s*danyang-match-admin\r?\n([\s\S]*?)(?=\r?\n {2}[a-z]|$)/, )?.[1] assert.ok(prodAdminBlock, '未能从 docker-compose.prod.yml 切出 admin-web 服务块(守卫自身失效)') const prodAdminMapping = prodAdminBlock.match(/-\s*"([^"]+)"/)?.[1] @@ -130,8 +130,13 @@ if (!existsSync(resolve(root, prodComposePath))) { // 端口改了、脚本没同步,不会有任何检查变红。现该变量已接到实测的管理端可达性检查上, // 这里再用门控保证它与 staging compose 的发布端口一致 —— 否则实测会稳定地测错对象。 const stagingCompose = read('infrastructure/docker-compose.staging.yml') +// 行尾必须容忍 CRLF —— 本仓库同时存在两种行尾(staging.yml / run-uat.sh 是 CRLF, +// prod.yml 是 LF),而 prod 那段在 CI 上因文件被 gitignore 根本不执行, +// 所以只有 staging 这段会在 Windows 工作区暴雷。若写死 \n: +// 窗口内夹了 \r 后解析直接失败,守卫会报「自身失效」—— +// 那是**假红**(真实端口 13521 与 run-uat.sh 一致),比不检查更坏。 const stagingAdminMapping = stagingCompose - .match(/container_name:\s*danyang-match-staging-admin\n([\s\S]*?)(?=\n {2}[a-z]|$)/)?.[1] + .match(/container_name:\s*danyang-match-staging-admin\r?\n([\s\S]*?)(?=\r?\n {2}[a-z]|$)/)?.[1] ?.match(/-\s*"([^"]+)"/)?.[1] assert.ok(stagingAdminMapping, '未能从 docker-compose.staging.yml 的 admin-web 解析出端口映射(守卫自身失效)') const stagingAdminPort = stagingAdminMapping.split(':').at(-2)