diff --git a/T-258.-.md b/T-258.-.md new file mode 100644 index 0000000..55aaf69 --- /dev/null +++ b/T-258.-.md @@ -0,0 +1,224 @@ + +> 同步来源:[`docs/tasks/T-258.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/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`: + +```go +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` 个一致)。 + +### 二、防凑数上限一律保留 + +以下既有校验**一条都不能放松**: + +```go +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`。