docs: import wiki at afc651f75a3a

ila
2026-08-07 16:37:03 +08:00
parent 8ee36371df
commit 09067a8684
+224
@@ -0,0 +1,224 @@
<!-- docs-wiki-sync:docs/tasks/T-258.md@afc651f75a3abc2676bb13aa8a80f6aa6a25a72e -->
> 同步来源:[`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`。