12 KiB
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.mjs24 项中 23 通过 / 1 失败(ui-contracts.mjs)。 - 活栈容器
danyang-match-backend(imageinfrastructure-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/<a>/<b>)。
任务 P0-1:解除 service_eligibility 死锁(最高优先级)
根因(已验证)
backend/src/main/java/com/danyang/match/platform/account/service/ServiceConsentService.java:75:
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 表:
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 条件是:
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_matchtest_one_side_confirm_no_matchtest_both_confirm_creates_match_episodetest_existing_active_match_not_duplicatedtest_prior_unmatch_creates_new_episodetest_one_side_decline_no_matchtest_timeout_handled
任务 P0-3:修复 6 个后端测试失败
P0-3a:MigrationValidationTest.testMigrationNamingConvention
backend/src/test/java/com/danyang/match/platform/deployment/MigrationValidationTest.java:46:
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 无此两行):
.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,搜索返回空。
修复(二选一):
- 推荐:在该测试的 fixture 里补
p.setPublishedProfileRevision(p.getProfileRevision());(对每个被搜索的用户 profile 都做),让测试与新契约一致。 - 备选:若
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 等)一次性提交:
# 本机 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 定版后的值)一致的版本号。
验收标准(全部满足才算完成)
mvn -B -o test:935 用例,0 失败 / 0 错误(17 跳过允许保留)。pytest tests/e2e -q(活栈重建后):72 用例,0 失败。node scripts/run-ui-contracts.mjs:24/24 通过。node node_modules/typescript/bin/tsc --noEmit(miniapp):0 错。node node_modules/vue-tsc/bin/vue-tsc.js --noEmit(admin-web):0 错。- 活栈重建后
GET /api/v1/health/ready=READY,GET /api/v1/version返回非0.0.1-SNAPSHOT的版本号(P1 完成时)。 git status --porcelain在fix-v12-gates分支上为空(全部已提交)。- 推送后 CI(若有)对
fix-v12-gates分支跑一遍全量绿。