# V1.2 在途代码修复任务提示词(交给 codex 执行) > 前置评审依据:`docs/RELEASE_REVIEW_2026-10-24.md`(若存在)与本轮审计证据。 > 本提示词**自包含**:codex 无需访问其它会话即可执行。 --- ## 背景(必读) 本仓库处于 V1.1 冻结基线(HEAD `d51c4c6`,tag `demo-v1.1.0-20261019`)之上,工作区有 **107–110 项未提交改动**,全部是 V1.2 在途开发。当前状态: - 后端 `mvn -B -o test` 实跑 **935 用例,6 失败 / 0 错误 / 17 跳过**。 - E2E `pytest tests/e2e -q` 实跑 **72 用例,27 失败 / 45 通过**。 - UI 契约门禁 `node scripts/run-ui-contracts.mjs` 24 项中 **23 通过 / 1 失败**(`ui-contracts.mjs`)。 - 活栈容器 `danyang-match-backend`(image `infrastructure-backend`,2026-10-24 23:07 构建)编译自当前工作区代码,`/health/ready` = READY,`/health/live` = 200。 - 数据库 `danyang_match` 实测 1010 用户全部 `service_eligibility_status = 'pending'`,0 个 `eligible`。 **本机 Git 注意**:分支名必须用**顶层单段名**(如 `fix-v12-gates`),禁止带 `/` 的嵌套分支名(本机监听进程会 0.2s 内删除 `refs/heads//`)。 --- ## 任务 P0-1:解除 service_eligibility 死锁(**最高优先级**) ### 根因(已验证) `backend/src/main/java/com/danyang/match/platform/account/service/ServiceConsentService.java:75`: ```java if (!"eligible".equals(user.getServiceEligibilityStatus())) throw blocked("当前账号不具备服务资格"); ``` 该门禁被 13 个控制器约 50 处调用(`RecommendationController`、`MatchController`、`LikeController`、`ChatController`/`MessageController`/`ConversationController`、`ActivityInterestController`、`CounselingController`、`AppointmentController`/`ServiceAppointmentController`、`ServiceProgramController`、`DiscoverController`、`MatchmakerController`、`PassController`、`CapabilityService`)。 全仓库扫描证实:**没有任何生产代码路径把 `users.service_eligibility_status` 置为 `"eligible"`**——只有 `UserRegistrationService.java:39` 和 `UserOnboardingService.java:36` 置 `"pending"`;`ReviewService.approveLocalEligibility`(`backend/src/main/java/com/danyang/match/platform/review/service/ReviewService.java:380-419`)只更新 `local_eligibility` 表的 `verification_status`,**不写 `users` 表**。 结果:任何真实用户完成全部注册/认证/授权流程后,仍被该门禁永久挡住,match/chat/counseling/appointment 等全部核心 API 返回 403。 ### 修复要求(二选一,推荐方案 A) **方案 A(推荐,最小改动)**:在 `ReviewService.approveLocalEligibility` 方法内,`localEligibilityMapper.updateById(el)` 之后**同步更新 `users` 表**: ```java user.setServiceEligibilityStatus("eligible"); userMapper.updateById(user); ``` - 需要注入 `UserMapper`(如尚未注入)。 - 同步修改 `rejectLocalEligibility`(若存在):`user.setServiceEligibilityStatus("ineligible")`。 - 补充或更新 `backend/src/test/java/com/danyang/match/platform/review/` 下的对应单测,断言 `users.service_eligibility_status` 确实被更新。 - 更新 `backend/src/test/java/com/danyang/match/platform/account/ServiceConsentGateTest.java:39` 的注释,说明测试不再需要直接置值(或保留为 fixture 辅助但加注释标注"这是测试快捷方式,生产路径由 ReviewService 负责")。 **方案 B(备选,若方案 A 的产品语义不合适)**:从 `ServiceConsentService.requireActive` 中**临时摘掉**第 4 项校验(`:"eligible"` 断言),让前 3 项(账号状态/手机、资料 published revision、双授权)作为当前门禁,并把 eligibility 写入路径作为独立 P1 任务。 ### 验证 - 在活栈 DB 手工把任意一个用户 `service_eligibility_status` 改为 `'eligible'`,重跑 E2E 中该用户相关的 27 个失败用例,确认 403 消失、链路可用。 - 或:修改 `tests/e2e/conftest.py` 里 E2E 用户种子逻辑,使其通过 `ReviewService` 路径获得 eligible 状态,再全量跑 `pytest tests/e2e`。 --- ## 任务 P0-2:修复活动互选 E2E 7 用例失败 ### 根因(已验证) `backend/src/main/java/com/danyang/match/platform/activity_mutual/service/ActivityMutualConsentService.java` 中: `createMutualConsent`(:37-67)按 `userId.compareTo(partnerId)` 排序后统一写 `userAId`/`userBId`,**只生成一行记录**(A→B 和 B→A 都指向同一行)。 `confirmConnection`(:70-104)调用 `consentMapper.confirmSideIfPending(consent.getId(), userId)`(`backend/src/main/java/com/danyang/match/platform/activity_mutual/mapper/ActivityMutualConsentMapper.java:57-69`),该 SQL 的 WHERE 条件是: ```sql WHERE id = #{id} AND deleted_at IS NULL AND both_confirmed_at IS NULL AND (expires_at IS NULL OR expires_at > NOW()) AND CASE WHEN user_a_id = #{userId} THEN user_a_status ELSE user_b_status END = 'pending' ``` SQL 本身是**对称的**(谁 confirm 就改谁那侧),看似没问题。但 E2E `tests/e2e/activity_mutual_consent_test.py` 中 A 先 confirm 后,B 再 confirm 时**拿到的状态仍停在 `pending`**,`bothConfirmed` 永远 `false`。 排查方向(codex 执行前先精读): - 确认 `ActivityInterestController` / `ActivityMutualConsentController`(在途新增)里 `createMutualConsent` 的调用时机与 `confirmConnection` 之间是否有**状态回滚**(比如 A confirm 后某处把 user_a_status 又写回 pending)。 - 确认 `ActivityMutualConsent` 实体(`??` 未跟踪)的 `both_confirmed_at` 字段与 H2/PG 的 `NOW()` 行为。 - 确认 `expires_at` 写入逻辑(`createMutualConsent` :60-63)是否在测试场景下导致 `(expires_at IS NULL OR expires_at > NOW())` 恒不成立。 ### 验证 跑 `pytest tests/e2e/activity_mutual_consent_test.py`,7 个用例全部通过: - `test_mutual_interest_does_not_create_match` - `test_one_side_confirm_no_match` - `test_both_confirm_creates_match_episode` - `test_existing_active_match_not_duplicated` - `test_prior_unmatch_creates_new_episode` - `test_one_side_decline_no_match` - `test_timeout_handled` --- ## 任务 P0-3:修复 6 个后端测试失败 ### P0-3a:`MigrationValidationTest.testMigrationNamingConvention` `backend/src/test/java/com/danyang/match/platform/deployment/MigrationValidationTest.java:46`: ```java private static final int LATEST_MIGRATION = 67; ``` 但 `backend/src/main/resources/db/migration/` 目录下 V062–V069 共 8 个文件全部 `??` 未跟踪,实际最大版本号是 **69**。 修复:把 `LATEST_MIGRATION` 改为 `69`,并在注释里补一行说明 V068、V069 分别对应 `grant_matchmaker_workbench_read_permissions` 和 `grant_matchmaker_workbench_operation_permissions`。 ### P0-3b:`DiscoverBlockInvisibilityIntegrationTest` × 5 `backend/src/main/java/com/danyang/match/platform/discover/service/DiscoverService.java:43-44`(V1.2 在途改动,HEAD 无此两行): ```java .isNotNull(UserProfile::getPublishedProfileRevision) .apply("profile_revision = published_profile_revision") ``` 导致测试 fixture(`backend/src/test/java/com/danyang/match/platform/discover/DiscoverBlockInvisibilityIntegrationTest.java:195`)只 set `profileStatus=APPROVED`,未 set `publishedProfileRevision`,搜索返回空。 修复(**二选一**): 1. **推荐**:在该测试的 fixture 里补 `p.setPublishedProfileRevision(p.getProfileRevision());`(对每个被搜索的用户 profile 都做),让测试与新契约一致。 2. 备选:若 `published_profile_revision` 语义尚未完全落地,在 `DiscoverService` 里把 `.isNotNull(...)` 改为 `.and(w -> w.isNotNull(UserProfile::getPublishedProfileRevision).or().apply("profile_revision = published_profile_revision"))`(放宽为"有 published revision 或 revision 等于 published 都可见")。**选此方案必须同步补注释说明这是过渡期放宽,并加 TODO 指向任务 P0-1 完成后收紧。** 跑 `mvn -B -o test -Dtest=DiscoverBlockInvisibilityIntegrationTest` 验证 5 个用例全绿。 --- ## 任务 P0-4:修复 UI 契约门禁失败 `ui-tests/ui-contracts.mjs`(或未跟踪的 `scripts/run-ui-contracts.mjs` 中)对 `miniapp/app.json` 的 `pages` 数组长度断言为 `=== 67`,但 V1.2 新增了 `pages/register/register` 和 `pages/service-consent/service-consent` 两页,实际为 **69**。 修复:把断言目标值从 67 改为 69。同时检查是否有**其它** UI 契约断言(如 tabBar 数量、特定页面路径白名单)需要同步更新。 验证:`node scripts/run-ui-contracts.mjs` 应输出 24/24 通过。 --- ## 任务 P0-5:提交 V1.2 在途改动并推送 完成 P0-1 ~ P0-4 后,把全部 107–110 项未提交改动(含 V062–V069 八个迁移、47 个后端未跟踪文件、`UserCreateView.vue`、`UserImportView.vue`、`miniapp/pages/register/`、`miniapp/pages/service-consent/`、`ServiceConsentController/Service/Controller` 等)一次性提交: ```bash # 本机 Git:分支名必须是顶层单段名,禁止 feat/xxx 形式 C:\Users\13070\.workbuddy\binaries\PortableGit\versions\1.3.0\cmd\git.exe checkout -b fix-v12-gates C:\Users\13070\.workbuddy\binaries\PortableGit\versions\1.3.0\cmd\git.exe add -A C:\Users\13070\.workbuddy\binaries\PortableGit\versions\1.3.0\cmd\git.exe commit -m "fix(v1.2): unblock service consent gate + mutual consent + test regressions + UI contract" # 本机分支带斜杠会被监听进程 0.2s 删除,单段名 fix-v12-gates 是安全的 C:\Users\13070\.workbuddy\binaries\PortableGit\versions\1.3.0\cmd\git.exe push origin fix-v12-gates ``` 推送前先验证 `C:\Users\13070\.workbuddy\binaries\PortableGit\versions\1.3.0\cmd\git.exe branch -vv` 显示 `main` 领先 `origin/main` 28 个提交的情况是否已在本次 `fix-v12-gates` 分支基础上解决(即新分支应从最新 main 切出并包含全部在途改动)。 --- ## 任务 P1(可选,若时间允许) - **部署链路**(详见 release-ops 审计):`infrastructure/docker-compose.prod.yml` 补 healthcheck;`deploy.sh` 回滚改为按 digest;`MINIO_PUBLIC_ENDPOINT` 注入真实可达地址;`backend/Dockerfile:7` 去掉 `-DskipTests`(或在 CI 单独跑全量测试);`nginx-production.conf:55-60` 恢复 80→443 跳转;`infrastructure/certs/` 目录补齐证书产物或写入 CI 生成步骤。 - **密钥治理**:`application.yml:88-89` 的 `admin/admin` 兜底、`docker-compose.staging.yml:118` 的硬编码 `ENCRYPTION_KEY`、`application-dev.yml:23,28,32,34` 的默认值全部改为 `${VAR:?}` 强制注入。 - **版本注入**:`backend/src/main/java/com/danyang/match/platform/.../VersionController.java:20` 的 `@Value("${app.version:0.0.1-SNAPSHOT}")` 改为 Maven resource filtering 注入,使 `/api/v1/version` 返回与 `VERSION=1.1.0`(或 V1.2 定版后的值)一致的版本号。 --- ## 验收标准(全部满足才算完成) 1. `mvn -B -o test`:**935 用例,0 失败 / 0 错误**(17 跳过允许保留)。 2. `pytest tests/e2e -q`(活栈重建后):**72 用例,0 失败**。 3. `node scripts/run-ui-contracts.mjs`:**24/24 通过**。 4. `node node_modules/typescript/bin/tsc --noEmit`(miniapp):**0 错**。 5. `node node_modules/vue-tsc/bin/vue-tsc.js --noEmit`(admin-web):**0 错**。 6. 活栈重建后 `GET /api/v1/health/ready` = `READY`,`GET /api/v1/version` 返回非 `0.0.1-SNAPSHOT` 的版本号(P1 完成时)。 7. `git status --porcelain` 在 `fix-v12-gates` 分支上为**空**(全部已提交)。 8. 推送后 CI(若有)对 `fix-v12-gates` 分支跑一遍全量绿。