1
T-258
ila edited this page 2026-08-07 16:37:03 +08:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

同步来源:docs/tasks/T-258.md · commit afc651f75a3a


id: T-258 title: 后端放宽候选批次数量门槛以与 T-256 对齐 phase: 2 deps:

  • T-256
  • T-257 status: DOING created: 2026-07-31 context_ref: 42a56ea work_branch: null write_paths:
  • docs/tasks/T-258.md
  • docs/api.md
  • docs/current-state.md
  • backend-api/internal/usecase/**
  • backend-api/internal/usecase/execution_result_service_test.go

问题 / 背景

T-256 放宽了 Android 侧的候选提交门槛(第三轮结束时至少一个已解析候选即可提交), 但后端仍在执行旧的「必须凑够 2 个」规则,两侧口径不一致,导致真机任务永久卡死。

backend-api/internal/usecase/execution_result_service.go:735-737:

return !command.CollectionComplete ||
    exact == 2 ||
    (command.SearchRound == 3 && fallback == 2)

即 collection_complete=true 时必须恰好 2 个 EXACT_MATCH,或第三轮恰好 2 个 FALLBACK,否则整批拒收。

真机实测的死锁

execution 9c634321-7f77-49d9-9444-3ccd2104c104,设备侧状态:

attempt_count   = 3       三轮跑完
terminal_queued = false   未写失败终态
completed       = true    门禁标记成功完成
候选数 = 1:ordinal=1 round=1 EXACT_MATCH goodsId=970618634860 双证据齐全

设备 logcat:

ProcurementApiException: 后台拒绝了请求
  at ProcurementApiClient.uploadExecutionOutboxItem(ProcurementApiClient.kt:333)
  at ProcurementRepository.flushExecutionOutbox(ProcurementRepository.kt:1675)

结果是双方都认为自己没错:

端 状态
App completed=true,不再写失败终态,outbox 无限重试上传
后端 持续拒收整批,从未收到候选
Admin 任务永久停在 RUNNING,不会自行恢复

重跑无效——换一个 execution 仍然只有 1 个候选,仍然被拒。

契约文档与实现不符

docs/api.md 关于 collection_complete 只写了:

collection_complete=true 才在同一事务中生成决策数据集、记录 CANDIDATES_READY 并将任务切换到 WAITING_CONFIRMATION。

完全没有提到「必须 2 个」这条限制。 后端实际执行的规则比文档严格,属于未文档化的 隐式约束,本任务一并修正。

与需求的冲突

docs/02-requirements.md:

  • 范围决策:「以 SKU 颜色/尺码硬约束过滤后回传 0..5 个」
  • 业务规则 20:「回传可以少于 5 个,禁止用弱匹配凑数」

需求要求的是「不足就少回传」,后端实现成了「不足就整批拒收」。

关联需求与交互

  • 功能:F-005、F-006、F-007
  • 用户故事:US-004、US-005
  • 交互:沿用 Admin 候选确认区,不新增页面
  • 架构/API:修改 POST /api/v1/tasks/{task_id}/candidates 的校验规则, docs/api.md 必须同步

行为契约

一、放宽数量门槛

validIncrementalCandidateSet 的最终返回改为:collection_complete=true 时 至少一个带 match_type 的已解析候选即可接收,不再要求恰好 2 个。

annotated == 0 的既有提前返回保持不变(零候选批次仍然接受,语义为「搜索完成但 无匹配」,与需求允许回传 0..5 个一致)。

二、防凑数上限一律保留

以下既有校验一条都不能放松:

annotated != len(command.Candidates) ||
exact > 2 || fallback > 2 || annotated > 3 ||
(fallback > 0 && command.SearchRound != 3) ||
(exact == 2 && fallback > 0)

放宽的只是下限,上限、轮次归属和 EXACT/FALLBACK 互斥全部维持原状。

三、其余校验不变

validCandidate、validCandidateCollectionMetadata、validPinduoduoCandidateProduct、 validEvaluation、validSKUMatchedCandidate、goods_id 去重、证据 asset 哈希核对 全部保持现状,一个字节都不改。

四、契约文档同步

docs/api.md 中 collection_complete 段落必须补明实际规则:接收 1..3 个带 match_type 的候选(EXACT_MATCH 至多 2 个且只能来自前两轮,FALLBACK 至多 2 个且 只能来自第三轮,两者不得同时达到上限),不再要求达到某个固定数量。

五、卡死任务的恢复

修复后无需重跑真机任务。App 的 outbox 仍在重试上传,后端一旦接收,execution 9c634321-7f77-49d9-9444-3ccd2104c104 应自动从 RUNNING 收敛到 WAITING_CONFIRMATION,候选 goodsId=970618634860 出现在确认区。这是本任务最直接的 验收信号。

方案

  1. 修改 execution_result_service.go 中 validIncrementalCandidateSet 的最终返回。
  2. 补充后端单测:1 个 EXACT_MATCH + collection_complete=true 被接收; 0 个 annotated 仍被接收;exact > 2、fallback > 2、annotated > 3、 FALLBACK 出现在非第三轮、exact == 2 && fallback > 0 全部仍被拒绝。
  3. 更新 docs/api.md 的 collection_complete 说明。
  4. 全量跑 go test ./... 确认没有依赖旧规则的既有测试被破坏——如有,逐个确认是 规则变更的预期影响,并在执行记录中列明。

验收要点

  • 1 个 EXACT_MATCH 候选 + collection_complete=true 被后端接收,任务转 WAITING_CONFIRMATION。
  • 0 个 annotated 候选的批次行为与本任务前一致。
  • exact > 2、fallback > 2、annotated > 3 仍被拒绝。
  • FALLBACK 出现在非第三轮仍被拒绝。
  • exact == 2 && fallback > 0 仍被拒绝。
  • goods_id 重复、证据哈希不符、match_type 与轮次不符仍被拒绝,有回归测试。
  • docs/api.md 的 collection_complete 说明与实现一致。
  • GOTOOLCHAIN=local go test ./... 在 backend-api 全部通过。
  • 根 init.ps1 通过(本任务涉及后端,该流程适用)。
  • 真机验证:无需重跑任务,execution 9c634321-7f77-49d9-9444-3ccd2104c104 自动收敛到 WAITING_CONFIRMATION,候选 goodsId=970618634860 出现在 Admin 候选确认区且可选择。

边界

  • 只放宽下限,不动上限:exact > 2、fallback > 2、annotated > 3、轮次归属和 EXACT/FALLBACK 互斥全部保留。
  • 不改 validCandidate、validCandidateCollectionMetadata、 validPinduoduoCandidateProduct、validEvaluation、validSKUMatchedCandidate。
  • 不改证据 asset 绑定、哈希核对、goods_id 去重或截图受控留存。
  • 不改任务状态机、租约、claim、heartbeat 或取消逻辑。
  • 不改下单授权、订单 dry-run、授权价格策略或支付边界。
  • 不改数据库 schema,不加迁移。
  • 不改 Android 任何代码——T-256 已完成客户端侧,本任务只补后端。
  • 不改 Admin 模板或候选展示。
  • 不为绕过校验而放宽 SKU 颜色/尺码硬约束。

执行记录

开工后记录修改、命令、结果、环境、决策和 blocker。

2026-07-31

  • 状态:开工时由 TODO 改为 DOING;按任务要求未改为 DONE。
  • 修改:
    • validIncrementalCandidateSet 在原有提前返回和完整上限校验通过后直接接收,不再 要求完成快照恰好含 2 个 EXACT_MATCH 或第三轮恰好含 2 个 FALLBACK。
    • 原 annotated == 0 行为保持不变;annotated != len(candidates)、exact > 2、 fallback > 2、annotated > 3、第三轮外 FALLBACK 和 exact == 2 && fallback > 0 的拒绝表达式逐字保留。
    • 将既有“单个 exact 完成快照应拒绝”测试改为接受断言。这是本次下限契约变更的 预期影响,不是为规避失败而删断言;同时新增单个第三轮 fallback 接受、零候选既有 接受、全部六类上限/轮次/互斥拒绝、match type 轮次归属和 goods_id 去重回归测试。
    • docs/api.md 已补明:完成快照无需凑满固定数量,带 match_type 的已解析批次 接收 1..3 个且必须全部标注,零候选表示搜索完成但无匹配,并完整记录 exact/fallback 上限、轮次和混合限制。
    • docs/current-state.md 已同步实现、验证结果和待真机验证 blocker。
  • 明确保留的边界:未修改 validCandidate、validCandidateCollectionMetadata、 validPinduoduoCandidateProduct、validEvaluation、validSKUMatchedCandidate、 evidence asset/hash 校验或 goods_id 去重;未修改数据库 schema/migration、Android、 Admin、下单或支付代码。
  • 命令与结果(Windows PowerShell):
    • 在 backend-api 工作目录运行 $env:GOTOOLCHAIN='local'; go test ./internal/usecase (修改前基线):退出码 0,ok(cached)。
    • gofmt -w internal/usecase/execution_result_service.go internal/usecase/execution_result_service_test.go: 退出码 0。
    • $env:GOTOOLCHAIN='local'; go test ./internal/usecase(修改后):退出码 0, ok cmroubao/backend-api/internal/usecase 0.185s。
    • 在 backend-api 工作目录运行 $env:GOTOOLCHAIN='local'; go test ./...: 退出码 0,所有 Go package 通过;没有因本次规则变化而失败的其他既有 Go 测试。
    • 在仓库根目录运行 .\init.ps1:退出码 0;Android 单测和 Debug APK 构建 BUILD SUCCESSFUL(78 tasks),后端全量 Go 验证通过。输出包含既有 SDK XML 版本兼容 warning,不影响成功结果。
    • git diff --check:退出码 0,无 whitespace error。
    • 对本任务全部已修改文件执行 CRLF 字节扫描:退出码 0,结果 LF_ONLY。
  • 既有回归证据:全量 Go suite 中 TestDeviceExecutionResultsAreIdempotentAndAuditable 继续覆盖 evidence asset hash 不符拒绝、非完成快照保持 RUNNING、完成快照产生单个 CANDIDATES_READY 并切换 WAITING_CONFIRMATION;本任务没有修改该测试或其实现路径。
  • 环境:工作区 D:\chengma\cmroubao,Go 使用 GOTOOLCHAIN=local;未运行 git commit 或 push,未改动工作区中既有的无关未跟踪文件。
  • 未验证 / blocker:无法执行最终真机验证。仍需观察 execution 9c634321-7f77-49d9-9444-3ccd2104c104 的既有 outbox 在部署修复后自动上传, 任务收敛到 WAITING_CONFIRMATION,并确认 goodsId=970618634860 出现在 Admin 候选确认区;因此任务保持 DOING。