fix(organization): 修复创建机构审计缺操作人;补全机构修复的验证并部署 Docker 测试环境

一、机构审计归属缺陷(本轮活栈验收中新发现)
- 现象:audit_logs 中 CREATE_ORGANIZATION 的 operator_id 7/7 为 NULL,
  而 DISABLE/ENABLE_ORGANIZATION 均正确记录操作人。审计不可归属 = 无效审计。
- 根因:OrganizationController.create 持有 @AuthenticationPrincipal OperatorPrincipal,
  却只把 params 传给 Service,未透传 principal.id() → saveAuditLog(null, ...)。
- 修复:createOrganization(params, operatorId) 签名透传;新增测试断言
  createOrganizationRecordsOperatorInAuditLog 长期守卫。
- 复验:新写入的 7 条 CREATE 审计全部带 operator_id,旧 14 条仍为 NULL(前后对照清晰)。

二、补全 rc.22 的验证缺口(此前记为「已修复待验证」)
- 全量回归 757 / 0 / 0 / 3,BUILD SUCCESS,108 个测试类,6:44(基线 741 + 新增 16)。
- 负向注入:ORG_STATUS_ACTIVE 改回 "normal" → 16 例变 3 失败 + 7 错误
  (守卫三条断言各自命中 + 服务侧 7 例被 H2 CHK_ORG_STATUS 拦下)。
  注入采用「仓库只读挂载 + 容器内副本改写」,工作区哈希前后一致,源码零污染。
- 活栈实测(Docker 全栈):创建/停用/启用均 2xx、非法输入 400、不存在 404、全程零 5xx(15/15)。
- 对已部署栈 pytest tests/e2e → 65 passed。

三、Docker 测试环境部署与工具链固化
- 测试镜像 danyang-match-backend-test 与命名卷 danyang-m2-cache 因 Docker Desktop 重装丢失,
  构建定义此前是临时产物 → 落盘为仓库资产 infrastructure/test-toolchain/Dockerfile。
- 本地栈统一使用 docker compose -p danyang(原项目名 infra 与另一项目撞名,
  其 --remove-orphans 会误删本项目容器;已确认邻项目数据无丢失,见 known-risks KR-09)。

四、UI 门禁脆弱性修复
- ui-document-baseline-contracts.mjs 解析 docker-compose.staging.yml 时报假红
  (「守卫自身失效」);真因是该文件为 CRLF 而守卫正则要求 \n 紧随 container_name 之后
  (prod.yml 是 LF 所以此前能过)。解析改为行尾无关,真实断言未削弱。门禁 18/18。

五、文档
- STATUS.md 新增 rc.23 并将 rc.22 由「待验证」订正为「已验证关闭」。
- 实施计划附 B.1、PROGRESS-ASSESSMENT-2026-09-21.md 同步订正。
- known-risks.md:KR-07 解除(Docker 已恢复)、KR-01/KR-06 按复测订正,
  新增 KR-08(Docker Hub 域名 DNS 被投毒,改用国内镜像源绕行)与 KR-09(compose 项目名撞名)。

验证状态:全量回归 757/0/0/3 全绿;UI 契约门禁 18/18;对已部署栈 E2E 65/65;
负向注入可变红;工作区哈希未被注入过程污染。
未验证:GitHub Actions 首次真绿(仍未推送,见 KR-06)。
This commit is contained in:
WorkBuddy
2026-09-22 00:46:10 +08:00
parent c195133680
commit 8bd1ab804a
9 changed files with 111 additions and 31 deletions
@@ -35,7 +35,7 @@ public class OrganizationController {
@PostMapping
public ApiResponse<?> create(@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, Object> params) {
return ApiResponse.ok(organizationService.createOrganization(params));
return ApiResponse.ok(organizationService.createOrganization(params, principal.id()));
}
@PutMapping("/{id}")
@@ -78,8 +78,16 @@ public class OrganizationService {
return organizationMapper.selectById(id);
}
/**
* 创建机构。
*
* <p>⚠️ {@code operatorId} 必须由 Controller 从认证主体透传:本方法会写
* {@code CREATE_ORGANIZATION} 审计行,缺失操作人即审计不可归属。
* 2026-09-21 活栈验收实测发现该参数原先未透传 —— 7 条 CREATE 审计的
* {@code operator_id} 全为 NULL,而停用 / 启用却正确记录,属「同一族缺陷的创建分支」。
*/
@Transactional
public Organization createOrganization(Map<String, Object> params) {
public Organization createOrganization(Map<String, Object> 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;
}
@@ -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<String, Object> params(String name, String type) {
Map<String, Object> 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<AuditLog> logs = auditLogMapper.selectList(new LambdaQueryWrapper<AuditLog>()
.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
+24 -11
View File
@@ -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 — 需用户/环境配合
+2 -1
View File
@@ -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` |
---
+7 -5
View File
@@ -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 按复测结果订正。*
@@ -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()`
+16
View File
@@ -0,0 +1,16 @@
# 后端测试工具链镜像 —— 本机无 JDK / 无 Maven,Java 测试一律经此容器执行。
#
# 构建:
# docker build -t danyang-match-backend-test:latest infrastructure/test-toolchain
#
# 运行(两处必挂,漏任一处都会导出完全错误的结论,见 2026-09-18 实施计划 附 B-9):
# -v "<repo>":/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
+7 -2
View File
@@ -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)