同步来源:
docs/tasks/T-258.md· commitafc651f75a3a
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 出现在确认区。这是本任务最直接的
验收信号。
方案
- 修改
execution_result_service.go中validIncrementalCandidateSet的最终返回。 - 补充后端单测:1 个 EXACT_MATCH +
collection_complete=true被接收; 0 个 annotated 仍被接收;exact > 2、fallback > 2、annotated > 3、 FALLBACK 出现在非第三轮、exact == 2 && fallback > 0全部仍被拒绝。 - 更新
docs/api.md的collection_complete说明。 - 全量跑
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。