Admin 侧从建商品到接收采集结果整条链路已经通了,但 Client 领不到任务。
实测现状:在 Admin 的 PDD 商品页点「创建采集任务」,任务确实建出来了 (tasks 表里状态 pending、assigned_client 为 NULL), Admin 的采集采购页也能看到,但它会一直停在「待分配」——没有客户端来领。
tasks
pending
assigned_client
① HttpAdminGateway 从设计上就不含领取能力
HttpAdminGateway
class HttpAdminGateway(ClientRegistrationGateway): """通过 HTTP 登记 Client;访问令牌只保存在内存。"""
它继承的是 ClientRegistrationGateway——只有 register_client 一个方法的窄接口, 注释写着「设置页只依赖登记能力,不依赖任务领取和提交」。
ClientRegistrationGateway
register_client
完整的 AdminGateway 有 4 个方法,目前只有 MockAdminGateway 全实现了。
AdminGateway
MockAdminGateway
② 界面上「获取任务」按钮的信号没人接
pdd_ui.py:276 autoFetchRequested = pyqtSignal() pdd_ui.py:409 self.autoFetchButton.clicked.connect(self.autoFetchRequested.emit)
autoFetchRequested 全项目只有这两处——定义和发射,没有任何地方监听。
autoFetchRequested
③ 没有任何生产代码调用 claim_next
claim_next
做:
不做(各自独立工单):
submit_result
submit_failure
settings_ui.py
settings_ui_event.py
android_device_service.py
Admin 实际返回的报文(admin/handler/api/client_api.go 的 taskPayload):
admin/handler/api/client_api.go
taskPayload
{ "task": { "id": "COL-8020a8729f111c15", "type": "collect", "version": 1, "priority": 0, "payload": { "goods_id": "737116531267", "goods_url": "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267" }, "created_at": "2026-08-07T03:19:49Z", "updated_at": "2026-08-07T03:19:49Z" } }
而 Client 的 AdminTask(client/src/admin_gateway.py:71)字段是 task_id 和 task_type。
AdminTask
client/src/admin_gateway.py:71
task_id
task_type
[必须] 映射必须显式写,不要用 AdminTask(**data) 这种自动展开—— 字段名对不上会得到 task_id=None,而 __post_init__ 抛的是 「task_id 不能为空」,看不出真正原因是字段名不一致。
[必须]
AdminTask(**data)
task_id=None
__post_init__
[必须] 测试里用上面这段真实报文做输入,不要自己编一个字段名恰好对得上的。 这是唯一能挡住这类错误的办法。
[必须] 没有可领的任务时 Admin 返回 204 No Content,不是 200 加空对象 (client_api.go:134 有注释说明)。
client_api.go:134
claim_next 收到 204 要返回 None,界面显示「暂无可领取的任务」这类中性提示, 不得当成错误弹窗。新客户端第一次来必然走到这里,是正常情况。
None
[必须] claim 请求里 supported_types 只填 ["collect"],写死,不要从设置里读。
supported_types
["collect"]
理由:client/AGENTS.md 写着「『获取任务』会真的去操作手机、可能下单」。 本工单只领不执行,所以这个按钮仍然是安全的。但如果现在就把 purchase 报上去,等以后接上执行能力的那一刻,这个按钮会悄悄地 从安全变成会花钱,而且没有任何一次代码评审会注意到——因为改的是别处。
client/AGENTS.md
purchase
采购任务要等 docs/client/06-quality-security.md §3 的采购安全门禁满足后再开, 届时单独走工单。
docs/client/06-quality-security.md
client/AGENTS.md 描述的最终形态是「获取任务」点击后变「停止获取」, 循环领取执行。本工单不做这个循环。
[必须] 点一次只领一个任务。理由:本工单不执行任务,循环领会把 Admin 上 所有任务一次性领空,而它们一个都不会被执行——比领不到更糟, Admin 那边全变成 claimed 却永远等不到结果。
claimed
[必须] 按钮文案保持「获取任务」,不实现「停止获取」的切换。 在 docs/client/05-ui-specification.md 标注这是过渡状态,接上执行能力时再改。
docs/client/05-ui-specification.md
[必须] 这是根 AGENTS.md 的红线。用 QObject + moveToThread 的 Worker 写法, 模板见 02 架构 §5.1。 不得用 QThread 子类、QRunnable 或 threading。
AGENTS.md
QObject
moveToThread
QThread
QRunnable
threading
[必须] 窗口关闭时按 [02 架构 §5.2] 断开连接,防止迟到结果访问已销毁控件。
[必须] 顺序是:claim 成功 → task_repository.add_claimed_task → 刷新表格。
task_repository.add_claimed_task
[必须] 存库失败必须明确告诉用户任务号,例如:
任务 COL-8020a8729f111c15 已在服务端领取,但本地保存失败:<原因>。 请记下这个任务号联系维护者。
原因:claim 成功后 Admin 已经把任务标成 claimed,本地却没有记录—— 这个任务从此谁也不会执行,Admin 那边也看不出异常。 不报任务号的话,事后完全无法定位是哪一条。
[建议] 这个缺口(claim 成功但本地写库失败 → 任务悬空)本工单不解决, 在 docs/client/04-admin-api-contract.md 的「已知缺口」里记一条。
[建议]
docs/client/04-admin-api-contract.md
[必须] add_claimed_task 遇到相同 remote_task_id 会抛 DuplicateTaskError。 要捕获它并当成正常情况处理(提示「这个任务本地已有」),不是错误—— 不捕获的话界面会弹一个看不懂的异常。
add_claimed_task
remote_task_id
DuplicateTaskError
client/src/admin_gateway.py
client/src/http_admin_gateway.py
client/src/pdd_ui_event.py
client/test/test_http_admin_gateway.py
client/test/test_pdd_ui_event.py
[注意] client/src/pdd_ui.py 和 ui_main.py 工作区里有未提交的改动(不是你的)。 本工单不需要改这两个文件——autoFetchRequested 信号已经存在, 接线在 pdd_ui_event.py 里做。如果发现非改不可,先停下来说明原因。
[注意]
client/src/pdd_ui.py
ui_main.py
pdd_ui_event.py
payload
X-Client-Id
X-Request-Id
Content-Type
request_id
界面
红线
其他
全部从 client/ 目录执行。
client/
1. 语法与单元测试
C:/Python310/python.exe -m pytest test/ -q
2. 界面离屏冒烟
$env:QT_QPA_PLATFORM="offscreen" C:/Python310/python.exe -c "from src.ui_main import MainWindow; from PyQt5.QtWidgets import QApplication; app=QApplication([]); w=MainWindow(); print('OK'); app.quit()" Remove-Item Env:QT_QPA_PLATFORM
3. 真实联调(不需要手机)
先起 Admin:
cd D:\chengma\cmautobuy\admin go run .
在 http://127.0.0.1:8080/pdd 添加一个商品、勾选、点「创建采集任务」。
再起 Client,在设置页登记一次,回 PDD 页点「获取任务」。
期望:
[必须] 交付报告里要贴这三步的实际截图或输出, 以及 Admin 那边任务状态变化的证据。
回退:git revert。本工单不操作手机、不采集、不下单,但 claim 会把 Admin 任务改成 claimed; 最坏情况是 Admin 上几条任务停在 claimed。
git revert
PDDTaskPageEvent
代码已完成并进入待验收:
TaskClaimGateway
HttpAdminGateway.claim_next
id/type/payload
supported_types: ["collect"]
QObject + moveToThread
NewClaimedTask
pdd_ui.py
实现提交:51062f4 feat: 领取 Admin 采集任务并保存本地 (#30)
51062f4
feat: 领取 Admin 采集任务并保存本地 (#30)
130 passed
compileall
git diff --check
go test ./... -count=1
使用独立 18080 临时 Admin 进程连接现有本地 Admin 数据库,完成后已停止临时进程,原 8080 热重载进程未改动。
首次点击: status=已领取任务 COL-ce3fefe75ff3c2b0,已保存到本地任务列表 rows=1 saved=True 第二次点击: status=暂无可领取的采集任务 button=获取任务 Admin:pending -> claimed,assigned_client 与本机已保存 Client 编号一致 Client SQLite:task_type=collect,goods_id=737116531267,status=claimed
本次真实联调只改变了这条测试采集任务的领取状态,没有调用 uiautomator2/ADB,没有采集或下单。
本地任务归档已提交:
docs/task/30-client-领取-admin-创建的采集任务.md
a2fc150
docs: 归档任务 #30
父工单 #1、#2 已确认包含 #30,当前保持未勾选;用户验收后再关闭本工单并同步父工单。
用户已于 2026-08-11 明确验收通过。
eca9289
No dependencies set.
The note is not visible to the blocked user.
基本信息
要解决什么
Admin 侧从建商品到接收采集结果整条链路已经通了,但 Client 领不到任务。
实测现状:在 Admin 的 PDD 商品页点「创建采集任务」,任务确实建出来了
(
tasks表里状态pending、assigned_client为 NULL),Admin 的采集采购页也能看到,但它会一直停在「待分配」——没有客户端来领。
三层原因,逐层查证
①
HttpAdminGateway从设计上就不含领取能力它继承的是
ClientRegistrationGateway——只有register_client一个方法的窄接口,注释写着「设置页只依赖登记能力,不依赖任务领取和提交」。
完整的
AdminGateway有 4 个方法,目前只有MockAdminGateway全实现了。② 界面上「获取任务」按钮的信号没人接
autoFetchRequested全项目只有这两处——定义和发射,没有任何地方监听。③ 没有任何生产代码调用
claim_next做什么 / 不做什么
做:
claim_next不做(各自独立工单):
submit_result/submit_failure仍不实现)MockAdminGatewaysettings_ui.py/settings_ui_event.py/android_device_service.py—— 另一个会话正在改这几个文件
怎么做
字段名对不上,这是最容易踩的坑
Admin 实际返回的报文(
admin/handler/api/client_api.go的taskPayload):而 Client 的
AdminTask(client/src/admin_gateway.py:71)字段是task_id和task_type。[必须]映射必须显式写,不要用AdminTask(**data)这种自动展开——字段名对不上会得到
task_id=None,而__post_init__抛的是「task_id 不能为空」,看不出真正原因是字段名不一致。
[必须]测试里用上面这段真实报文做输入,不要自己编一个字段名恰好对得上的。这是唯一能挡住这类错误的办法。
204 不是错误
[必须]没有可领的任务时 Admin 返回 204 No Content,不是 200 加空对象(
client_api.go:134有注释说明)。claim_next收到 204 要返回None,界面显示「暂无可领取的任务」这类中性提示,不得当成错误弹窗。新客户端第一次来必然走到这里,是正常情况。
只报 collect,不报 purchase
[必须]claim 请求里supported_types只填["collect"],写死,不要从设置里读。理由:
client/AGENTS.md写着「『获取任务』会真的去操作手机、可能下单」。本工单只领不执行,所以这个按钮仍然是安全的。但如果现在就把
purchase报上去,等以后接上执行能力的那一刻,这个按钮会悄悄地从安全变成会花钱,而且没有任何一次代码评审会注意到——因为改的是别处。
采购任务要等
docs/client/06-quality-security.md§3 的采购安全门禁满足后再开,届时单独走工单。
一次只领一个,不做循环
client/AGENTS.md描述的最终形态是「获取任务」点击后变「停止获取」,循环领取执行。本工单不做这个循环。
[必须]点一次只领一个任务。理由:本工单不执行任务,循环领会把 Admin 上所有任务一次性领空,而它们一个都不会被执行——比领不到更糟,
Admin 那边全变成
claimed却永远等不到结果。[必须]按钮文案保持「获取任务」,不实现「停止获取」的切换。在
docs/client/05-ui-specification.md标注这是过渡状态,接上执行能力时再改。不得在主线程发 HTTP
[必须]这是根AGENTS.md的红线。用QObject+moveToThread的 Worker 写法,模板见 02 架构 §5.1。
不得用
QThread子类、QRunnable或threading。[必须]窗口关闭时按 [02 架构 §5.2] 断开连接,防止迟到结果访问已销毁控件。先存库再更新界面,存库失败要说清楚
[必须]顺序是:claim 成功 →task_repository.add_claimed_task→ 刷新表格。[必须]存库失败必须明确告诉用户任务号,例如:原因:claim 成功后 Admin 已经把任务标成
claimed,本地却没有记录——这个任务从此谁也不会执行,Admin 那边也看不出异常。
不报任务号的话,事后完全无法定位是哪一条。
[建议]这个缺口(claim 成功但本地写库失败 → 任务悬空)本工单不解决,在
docs/client/04-admin-api-contract.md的「已知缺口」里记一条。[必须]add_claimed_task遇到相同remote_task_id会抛DuplicateTaskError。要捕获它并当成正常情况处理(提示「这个任务本地已有」),不是错误——
不捕获的话界面会弹一个看不懂的异常。
预计修改文件
client/src/admin_gateway.pyclaim_next的窄接口(照ClientRegistrationGateway的写法)client/src/http_admin_gateway.pyclaim_next,含字段名映射和 204 处理client/src/pdd_ui_event.pyautoFetchRequested→ Worker → 存库 → 刷新client/test/test_http_admin_gateway.pyclient/test/test_pdd_ui_event.pydocs/client/04-admin-api-contract.mddocs/client/05-ui-specification.md[注意]client/src/pdd_ui.py和ui_main.py工作区里有未提交的改动(不是你的)。本工单不需要改这两个文件——
autoFetchRequested信号已经存在,接线在
pdd_ui_event.py里做。如果发现非改不可,先停下来说明原因。验收标准
claim_next
task_id/task_type/payloadNone,不抛异常X-Client-Id、X-Request-Id、Content-Typesupported_types只有["collect"]request_id界面
红线
QObject+moveToThread,不是QThread子类 /QRunnable/threading其他
settings_ui.py/settings_ui_event.py/android_device_service.pyMockAdminGateway怎么验证
全部从
client/目录执行。1. 语法与单元测试
2. 界面离屏冒烟
3. 真实联调(不需要手机)
先起 Admin:
在 http://127.0.0.1:8080/pdd 添加一个商品、勾选、点「创建采集任务」。
再起 Client,在设置页登记一次,回 PDD 页点「获取任务」。
期望:
「已领取」,客户端列显示你的客户端编号
[必须]交付报告里要贴这三步的实际截图或输出,以及 Admin 那边任务状态变化的证据。
风险和回退
回退:
git revert。本工单不操作手机、不采集、不下单,但 claim 会把 Admin 任务改成claimed;最坏情况是 Admin 上几条任务停在
claimed。开工前补充确认(2026-08-07)
PDDTaskPageEvent支持注入领取 Gateway 和 Settings Repository;生产默认读取同一 SQLite 配置,不修改当前脏状态的ui_main.py。实施记录(2026-08-07)
代码已完成并进入待验收:
TaskClaimGateway窄接口,HttpAdminGateway.claim_next显式映射 Admin 的id/type/payload字段;None,请求只声明supported_types: ["collect"];QObject + moveToThreadWorker,一次只领一个;NewClaimedTask显式转换和 SQLite 保存,成功后主线程刷新表格;pdd_ui.py、ui_main.py、Mock Gateway、设置页和 Android/手机代码。实现提交:
51062f4feat: 领取 Admin 采集任务并保存本地 (#30)自动验证
130 passed;compileall、离屏主窗口启动/关闭和git diff --check通过;go test ./... -count=1通过。真实联调输出(不连接手机)
使用独立 18080 临时 Admin 进程连接现有本地 Admin 数据库,完成后已停止临时进程,原 8080 热重载进程未改动。
本次真实联调只改变了这条测试采集任务的领取状态,没有调用 uiautomator2/ADB,没有采集或下单。
本地任务归档已提交:
docs/task/30-client-领取-admin-创建的采集任务.mda2fc150docs: 归档任务 #30父工单 #1、#2 已确认包含 #30,当前保持未勾选;用户验收后再关闭本工单并同步父工单。
用户已于 2026-08-11 明确验收通过。
eca9289