新增 000228 成对迁移 tb_polling_priority_item:卡、任务类型、状态、触发类型、来源订单/套餐使用记录、
触发次数与来源集合、尝试次数、失败原因、人工原因与操作者、店铺快照与各时间列;以活动项部分唯一索引
uq_polling_priority_item_active(仅 deleted_at IS NULL AND status IN ('pending','processing') 占键位)
表达「同卡同任务类型至多一条活动项」,另有状态/时间索引与全列注释;down 守卫在存在活动项或未终态行时
拒绝回滚并给出中文原因。
新增优先轮询请求可靠事件 polling.priority.requested(载荷版本 v1、事件键前缀 prio:)与消费者:只在原
业务事务内追加、幂等键稳定;消费者按卡 × 纳入任务类型(realname/carddata/card_status/package)逐条
建项并在提交后下发执行提示,重复投递只合并触发次数、来源集合与最近触发时间,不新建行也不重复调用。
触发点为四类自动场景 purchase_activated / renewal_activated(按同载体更早套餐使用记录判定)/
queue_activated / addon_activated 与「无有效套餐」no_valid_package(仅在普通套餐轮询来源且存在待生效
套餐使用记录时追加;事件通道显式拒绝 manual_trigger);入队对象恒为卡,绑定设备资产在触发事务内冻结
在用卡快照逐卡建项,不使用设备当前卡槽口径。
轮询共享基类新增认领接缝:四个 Handler(realname/carddata/card_status/package)在并发信号量之后、调用
上游之前探测活动项——待执行条件认领、执行中且 90 秒租约未到期则跳过并延后、无活动项时行为与既有完全
等价;超租约允许相邻执行接管,尝试次数只在真正发起执行后累加,未达上限(3)回到活动态按既有间隔重排,
达上限或业务校验类失败进入失败终态并保留可安全展示原因;执行前校验卡自身与绑定设备的轮询开关。未引入
通用卡级锁与 Redis 活动标记,分片队列的出队、入队与移除路径未改动。
提示通道按任务类型独立键(polling:priority:{taskType}),与既有手动触发队列分离;调度器在同一周期内先
排空优先提示、再排空手动触发队列,提示排空不受分片背压跳过影响;未新建调度设施或异步任务类型。
新增人工优先入队与只读查询三条路由 POST /api/admin/polling-priority-items、
GET /api/admin/polling-priority-items、GET /api/admin/polling-priority-items/:id:人工入队复用既有轮询
权限判定(抽取为同包共享函数),原因必填,不受每日 500 次上限与 24 小时去重约束,重复抑制由活动项合并
承担;读侧按店铺快照下推数据范围,越权与不存在不可区分,不提供优先级分级、有效期或人工重触发入口。
新增 7 个审计动作(enqueue/claim/fail/retry/complete/dequeue/manual_denied)与资源
polling_priority_item,并按(操作者类型,来源)注册,人工侧与 Worker 侧均通过来源校验。
同步 OpenAPI 文档装配三处与路由注册;归档 Change 至
openspec/changes/archive/2026-09-17-add-priority-polling-queue/ 并同步主 Spec(新增
priority-polling-queue、polling-operations 追加单次执行互斥 Requirement 与三条路由索引)与上下文健康
证据(requirement-evidence 150 行、入口矩阵 http 403 / async 56)。
本机验证:junhong_cmp_test 与隔离 Redis DB 15,未连生产、未启动 Worker/API、未调用运营商上游;迁移
up/down/up 与 down 守卫实测(含 dirty=true 记账口径与 force 恢复),A–F 批 94 PASS、接缝 63 PASS、
提示通道 12 PASS、清理零残留 20 PASS。成功路径 Complete、真并发互斥、尝试上限第 3 次判定、HTTP 层权限
矩阵、通道阈值持锁复机边界与三类生效触发点生产集成留待测试部署验证(见
docs/verification/add-priority-polling-queue-verification.md 第 4 节)。自动化测试按项目决策为 N/A,
未新增 *_test.go。
20 KiB
AUG26-016 卡轮询优先队列(add-priority-polling-queue)实施与验证记录
本文件记录 OpenSpec Change add-priority-polling-queue 的实施范围、已实测证据与未验证清单。
所有实测均在测试库 junhong_cmp_test 与隔离 Redis DB 15 上执行;未连接生产、未启动 Worker/API 进程、未调用运营商上游接口。
0. 结论概览
| 验证项 | 断言计数 | 结论 |
|---|---|---|
| 9.1 迁移 up/down/up + down 守卫 | 结构断言 4 组 + 探针 6 条 | 通过(含记账 dirty=true 的实测结论) |
| 9.2–9.7 本机可安全验证部分(A–F + H5 批) | 94 PASS / 0 FAIL | 通过 |
接缝执行(认领/跳过/接管/范围校验,非导出接缝经真实 Handle 驱动) |
63 PASS / 0 FAIL | 通过 |
提示通道(优先键与手动键、FIFO、RemoveFromAllQueues 边界、分片队列快照) |
12 PASS / 0 FAIL | 通过 |
| 清理与零残留(PG 六类计数 + Redis 键 + 越界 DB 只读复核) | 20 PASS / 0 FAIL | 通过 |
成功路径 Complete、真并发、尝试上限第 3 次判定、package handler 接缝、HTTP 层、通道阈值持锁、三类生效触发点生产集成 |
— | 未验证(见第 4 节) |
1. 环境与命令
- 目标库:
junhong_cmp_test(cxd.whcxd.cn:16159);迁移经scripts/migrate.sh(读取.env的DB_*)。 - 隔离 Redis:
cxd.whcxd.cn:16299DB 15(部署库为 DB 6、本地沙盒为 DB 7,二者对所有写入被显式拒绝)。 - 环境装载:
set -a; . ./.env; . ./.env.local; set +a(DB_*只在.env;Redis 参数只在.env.local)。 - 构建:
go build -o /tmp/pf ./cmd/priority-fixture-tmp。 - 只读诊断:dbhub MCP
mcp__postgres_execute_sql_main(仅 SELECT);Redis 只读用redis-cli -n <db> EXISTS|DBSIZE|SCAN。 - 临时工具子命令:
setup|a-store|b-consumer|c-trigger|d-read|e-enqueue|f-audit|g-h5|seam-verify|hint-verify|purge-stale-audits|cleanup|count|insert-fixture|delete-fixture|count-rows|index-probe。 该工具带有安全栏:DB_NAME必须为junhong_cmp_test,所有自建行带AUG26016标记,清理只按本批 ID 集合或标记删除。 - 日志:每场景独立带时间戳文件
run<N>-<步骤>-<TS>.log(不覆盖);本文只摘录关键原始行。
2. 已验证清单(含点时间与期望/实际)
2.1 任务 9.1:迁移 up / down / up 与 down 守卫(2026-09-16)
| 步骤 | 期望 | 实际(原文摘录) |
|---|---|---|
| 迁移前版本 | 227 | 当前迁移版本: 227 |
./scripts/migrate.sh up |
应用到 228,exit=0 | 228/u add_polling_priority_queue (344.53875ms)、✓ 迁移操作完成 |
| 结构 | 23 列全注释、5 索引(含活动项部分唯一索引)、9 CHECK、0 外键 | column_count=23、commented_columns=23、index_count=5、c:9+p:1、fk_count=0 |
| 活动项索引谓词 | 仅 deleted_at IS NULL AND status IN ('pending','processing') 占键位 |
CREATE UNIQUE INDEX uq_polling_priority_item_active ... WHERE ((deleted_at IS NULL) AND ((status)::text = ANY ((ARRAY['pending','processing'])::text[]))) |
造 1 条活动 fixture 后 ./scripts/migrate.sh down |
守卫拒绝回滚且结构零损伤 | pq: 存在卡轮询优先队列活动项,拒绝回滚以避免丢失在途加急事实与认领租约状态、down_exit=1;随后 table_regclass=tb_polling_priority_item、row_count=1、column_count=23、index_count=5 |
| 守卫后的记账标记 | —(实测为工具行为) | {"version":"227","dirty":true} |
| 记账修复 | force <真实版本> 只改一行、不动 DDL |
./scripts/migrate.sh force 228 → {"version":"228","dirty":false} |
清 fixture 后 down |
表/索引/relation 全部消失,exit=0 | 228/d add_polling_priority_queue (221.332417ms);{"table_regclass":null,"index_count":"0","relation_leftovers":"0"} |
再次 up |
回到 228 且结构完整 | {"version":"228","dirty":false,"column_count":"23","index_count":"5","check_count":"9","fk_count":"0"} |
实测结论(dirty=true 的口径):golang-migrate 在运行某个迁移文件之前先写入目标版本并标 dirty=true,成功后写回 dirty=false;down 228 的目标版本是 227,守卫 RAISE EXCEPTION 后库被留在 227, dirty=true。
这是迁移工具的记账行为,不是本 Change 的实现缺陷;恢复方式是 ./scripts/migrate.sh force <当前真实版本>(本例 228),该命令只修一行记账、不触发 DDL 或数据变更。验证 down 守卫的正确预期应写成「守卫拒绝 + 结构零损伤 + 由维护者 force 回当前真实版本并确认 dirty=false」。
2.2 部分唯一索引语义与约束探针(2026-09-16)
[成功] P1 插入活动行 A(id=1 card_id=6012 task_type=carddata status=pending)
[报错] P2 重复活动行(同卡同任务类型) → SQLSTATE=23505 constraint=uq_polling_priority_item_active
[成功] P3 软删 A 后插入活动行 B(id=3)→ 软删行不占用活动项键位
[报错] P4 插入活动行 C(B 仍活动) → SQLSTATE=23505 constraint=uq_polling_priority_item_active
[成功] P5 把 B 转终态(completed/success)后插入活动行 D(id=5)→ 终态行不占用活动项键位
[报错] P6 终态与结果一致性(completed 但 result 为空) → SQLSTATE=23514 constraint=ck_polling_priority_item_terminal_result
索引与约束探针全部符合预期 (exit=0)
2.3 A–F 批次(Store / 消费者 / 触发分类 / 读侧 / 人工入队 / 审计)——94 PASS / 0 FAIL
时间:2026-09-17 09:46:49(+08),日志 run5-<场景>-20260917T094649.log(另有一轮同结果运行 run-e2e-*-20260917T092447.log)。
| 场景 | 断言 | 期望 → 实际(摘录) |
|---|---|---|
| A Store 生命周期 | 20 PASS | A1 首次认领命中…→ status=processing claimed_at=2026-09-17 09:24:49+08;A2 超租约(120s>90s)允许接管 → taken=true;A2 租约内(10s<90s)不允许接管 → taken=false;A3 两次 IncrementAttempt 后 attempt_count → 期望=2 实际=2;A3 MarkFailed(未累加尝试次数,等同 FailFinal 路径)→ attempt_count=0;A3 尝试上限路径 → attempt_count=1;A4 分页归一化 Page=0/PageSize=999 → total=29 returned=20 |
| B 消费者 | 15 PASS | B5 设备载体在事务内冻结的绑定卡数 → 3;B5 活动项行数(3 卡 × 4 任务类型) → 12;B6 重复投递只合并:行数不变、触发次数+1、最近触发时间刷新、触发类型集合去重;B7 自动(no_valid_package)+ 人工(manual_trigger)合并为同一行;B8 已终态后重复追加同一 event_id:outbox 仍只 1 行;B9 同一 event_id 重投不产生重复审计行(EventID 去重) → 1 |
| C 触发分类与前置条件 | 13 PASS | C10 → renewal_activated / purchase_activated;C11 存在 status=0 → HasPendingPackageUsage=true;C12 观测抑制为真 → 不写事件(0 行);C12 无有效套餐 event_id ≤ 48 且主体与载体可区分 → prio:nvp:d:990016999:… len=31 |
| D 读侧权限与范围 | 12 PASS | D13 超管/平台 → total=18/18;D13 代理 → total=16(平台卡与范围外卡各 1 行不可见);D13 企业 → code=1005;D14 详情越权 vs 不存在 → 同一码与文案 |
| E 人工入队用例 | 14 PASS | E15 一次入队覆盖全部纳入类型 → created=4 merged=0;E16 24h 内再次入队 → created=0 merged=4;E17 原因缺失/超长 → CodeInvalidParam;E17 拒绝审计 → 5 条 manual_denied;E17 被拒绝的请求不产生优先项变更 → 仍 4 行活动项 |
| F 审计来源匹配 | 6 PASS | F polling_priority.enqueue/claim/manual_denied 注册 → primary=polling_priority_item,origins 含 account/admin_api 与 system_task/worker;F 消费者审计 Actor=system_task / Source=worker |
| count(残留计数) | 10 PASS | 卡/设备/绑定/套餐使用/优先项/审计/outbox/两类孤儿审计资源 均 = 0,且 本批全部自建行已清理干净 |
2.4 H5 加固:事件通道拒绝 manual_trigger(2026-09-17 09:24:47)
internal/infrastructure/prioritypolling/event.go 的 AppendPriorityRequested 在资源类型校验后显式拒绝
trigger_type='manual_trigger'(人工入队由 internal/service/polling/priority_enqueue_service.go 直写事实表,不经事件通道)。
[PASS] H5-1 经事件通道投递 manual_trigger 返回参数错误 → code=1001 err=优先轮询事件不接受人工入队触发类型 manual_trigger,人工入队请调用人工入队用例
[PASS] H5-3 该 event_id 在 tb_outbox_event 新增 0 行 → 期望=0 实际=0
[PASS] H5-4 tb_outbox_event 总行数不变(拒绝发生在写入前) → 期望=19453 实际=19453
2.5 接缝执行(seam-verify)——63 PASS / 0 FAIL
时间:2026-09-17 10:16:34(+08),隔离 Redis DB 15(打印 db=15),未执行 FlushDB。
驱动方式:不启动 Worker/Asynq/HTTP,用 task.NewPollingBase + asynq.NewTask 直接调用
PollingRealnameHandler / PollingCarddataHandler / PollingCardStatusHandler 的真实 Handle;
integration 注入真实仓库(非 nil),使「零上游」由真实计数断言证明。
每个任务类型(realname / carddata / card_status)依次断言 (a)–(e):
| 断言组 | 期望 → 实际(摘录) |
|---|---|
| 计数器真对照 | 写入 1 行 tb_integration_log 后按夹具卡统计 +1 → 期望=1 实际=1;对照行已删除:计数回到原值 → 期望=0 实际=0 |
| (a) 无活动项 = 基线等价 | Handle 正常返回 → err=<nil>;不产生优先项行 → 0;零上游调用 → 0;仍按既有路径重入队(分片 ZSET 命中) → key=polling:shard:0:queue:polling:realname member=11696 |
| (b) pending = 本次领取 | claim 审计 +1 → delta=1;status=failed claimed_at=2026-09-17 10:16:41+08 attempt=0 reason="流量查询能力未配置";fail+dequeue 各 +1;零上游调用 → 0 |
| (c) 租约内 = 跳过且零上游 | 状态/认领时间/尝试次数/失败原因全部不变 → processing,claimed_at 前后相同;claim=0 fail=0 dequeue=0;零上游调用 → 0;重新入队(ZSET 命中) |
| (d) 超租约 = 接管 | claimed_at=2026-09-17 10:16:42+08(距运行 <30s,已刷新);claim delta=1;零上游调用 → 0 |
| (e) 卡不在轮询范围 | status=failed reason="卡已不在轮询范围内" attempt=0;零上游调用 → 0 |
收尾(同一进程内):
[清理] 隔离 Redis 已删除本套件使用的 12 个键(未执行 FlushDB)
[清理] 审计事实(按本批 ID 集合精确删除):事件=27,资源行=27
[清理] 本功能动作码审计保留条数(非本批写入,未删除):0
[清理] 优先项=0;Outbox(prio:)=0;套餐使用=0;绑定=3;卡=6;设备=1
[PASS] 清理后计数 … 应为 0(9 条)+ 本批全部自建行已清理干净
结果:全部断言通过
2.6 提示通道(hint-verify)——12 PASS / 0 FAIL
时间:2026-09-17 10:16:34(+08),隔离 Redis DB 15、未 FlushDB、全程前后 polling:shard:* 快照 {}。
[PASS] ① EnqueuePriority 只写优先提示键(LLEN) → 期望=3 实际=3
[PASS] ① 手动触发键未被 EnqueuePriority 触碰(LLEN) → 期望=0 实际=0
[PASS] ① EnqueueManual 只写手动触发键(LLEN) → 期望=1 实际=1
[PASS] ① 优先提示键未被 EnqueueManual 触碰(LLEN) → 期望=3 实际=3
[PASS] ② RPush 3 元素后 LPopCount 弹出顺序为先进先出 → 期望=[990016101 990016102 990016103] 实际=[990016101 990016102 990016103]
[PASS] ③ 边界登记:RemoveFromAllQueues 不清理优先提示键(LLEN 保持 1)
[PASS] ③ 边界登记:RemoveFromAllQueues 不清理手动触发键(LLEN 保持 1)
[PASS] ④ 本子命令前后 polling:shard:* 键集合与成员完全一致(diff 为空) → before={} after={}
[PASS] 收尾 删除本子命令使用的键数 → 2;两个键 EXISTS=0(未残留)
2.7 清理与零残留(PG + Redis 只读复核,2026-09-17 10:17 起)
| 指标 | 运行前 | 运行后 |
|---|---|---|
tb_polling_priority_item |
0 | 0 |
标记夹具卡 / 设备(AUG26016%) |
0 / 0 | 0 / 0 |
polling_priority.% 审计 |
0 | 0 |
prio: outbox |
0 | 0 |
孤儿审计资源(polling_priority_item / iot_card+polling_card) |
0 / 0 | 0 / 0 |
schema_migrations |
228 / dirty=false | 228 / dirty=false |
Redis DB 15 polling:* 键数 |
0 | 0 |
Redis DB 15 本套件 12 键 EXISTS |
— | 0 |
Redis DB 6 夹具卡专属键(polling:card:11696、traffic:sync:lock:card:11696) |
— | 0 / 0(无本 Change 痕迹) |
库内全量孤儿审计资源另有 88 行,分组为 employee_collection_bill=37、employee_collection_application=16、
employee_collection_application_attempt=16、package_traffic_alert=16、package_traffic_alert_rule=3——
全部属于其它能力,本 Change 两类均为 0。
2.8 一次真实失败与其修复(如实记录)
首次接缝运行(2026-09-17 10:11:34,run6-seam-verify-20260917T101134.log)接缝断言 61/61 通过,
但收尾残留检查 2 条 FAIL:polling_priority.* 审计残留 27 条(claim 9 / fail 9 / dequeue 9,
created_at 2026-09-17T02:11:37Z–02:11:49Z)。
根因是验证工具自身的记账缺陷:场景逐个任务类型收尾会删除优先项行,而审计清理依赖按优先项 ID 反查,删行后即漏删。
修复(仅临时工具,未放宽任何断言):运行期采集优先项 ID 并在清理时作为额外 ID 集合传入;另加带时间窗护栏的
purge-stale-audits 兜底子命令(必须显式给 AUG26016_PURGE_SINCE,窗口外的行一律拒绝删除并列出)。
修复后重跑 63/63 全绿,27 条残留按兜底路径精确删除(事件=27,资源行=27,删除后残留=0)。
3. 工程门禁(收尾复跑,脚手架删除后;2026-09-17 10:26)
| 命令 | 原始结果 | 退出码 |
|---|---|---|
gofmt -l . |
仅列出 6 个历史未格式化文件:internal/model/dto/package_dto.go、internal/model/order_package_invalidate_task.go、internal/model/personal_customer_device.go、internal/model/personal_customer_iccid.go、internal/model/personal_customer_phone.go、internal/query/h5popup/query.go;本 Change 改动文件(internal/infrastructure/prioritypolling/event.go、internal/task/*、internal/polling/*、internal/service/*、pkg/constants/*、migrations/000228_* 等)均未出现 → 无新增未格式化文件 |
0 |
go build ./cmd/api ./cmd/worker |
无输出(仅 Go 模块缓存 stat 写入诊断,不影响构建) | 0 |
go vet ./... |
无诊断输出 | 0 |
go run cmd/gendocs/main.go(连续两次) |
两次均输出「成功在以下位置生成 OpenAPI 文档」;md5(docs/admin-openapi.yaml) = 696dba47834f93faacba93c745353fdd(两次一致;该文件被 .gitignore 忽略,是本地生成产物) |
0 / 0 |
openspec validate add-priority-polling-queue --strict |
Change 'add-priority-polling-queue' is valid |
0 |
openspec doctor --json |
"root": {… "healthy": true, "status": []}、"status": [] |
0 |
./scripts/context-health.sh |
Context 健康检查通过 |
0 |
自动化测试按项目决策为 N/A:未创建任何 *_test.go(context-health.sh 亦校验仓库无 *_test.go)。
4. 未验证清单(明确未覆盖,不得据本文推断)
| 项 | 未验证原因 |
|---|---|
优先项成功路径 Complete(processing → completed) |
需真实上游调用成功;本轮所有执行都停在「能力未配置」失败分支,未制造成功上游响应 |
| 真并发下的认领互斥 | 无并发执行环境;本轮以「租约内执行中 → 跳过且零上游」的单线程等价路径覆盖,未做同卡同类型真并发竞态 |
| 尝试上限第 3 次判定 | 需稳定失败的真实上游;本轮止于 FailFinal(尝试次数不累加),未覆盖 FailRetryable 连续 3 次的收敛 |
package handler 的接缝 |
其 Handle 依赖 t.ResultWriter().TaskID(),本地 asynq.Task 会 panic,未驱动 |
| HTTP 层权限矩阵 | 未启动 API;权限判定仅覆盖服务层与查询层(读侧范围、越权=不存在同一响应),未取得真实状态码/msg |
| 9.7 通道阈值持锁边界 | 需 tb_carrier_traffic_threshold_lock 活动锁与真实停复机评估路径;本轮未造锁、未驱动停复机 |
| 三类生效触发点的生产集成 | 新购/续购/加油包/排队顺延的触发分类与事件构造已单测(C 场景),但未经真实订单/支付/激活链路端到端验证 |
queue_activated / addon_activated 正向落库 trigger_type |
C 场景只对 purchase_activated/renewal_activated 做了正向落库;排队顺延与加油包仅验证了事件构造 |
| 调度器排空顺序与分片背压 | 需 Worker + 既有部署环境;本文只覆盖提示通道的生产/消费语义 |
| 迁移 down 守卫的自动化 | 未引入自动化测试(项目决策 N/A),仅为手工实测记录 |
5. 边界声明(避免误读)
tb_integration_log全表计数会被在跑的测试部署推高:观测窗口内该表由 5,905,167 增至 5,905,619(+452), 来源是并发运行的既有测试部署(DB 6 心跳/并发/27 卡在跑),不是本套件产生;本套件在该表只插入并删除 1 行对照行(复查 = 0)。- 「零上游调用」的判据是夹具卡维度:
tb_integration_log WHERE resource_type='iot_card' AND resource_id='<夹具卡ID>', 每条场景运行前后均为 0(15 条独立断言),并由counterControl对照证明该计数口径能发现写入。 这不等于「整库无上游调用」——测试部署的正常上游调用一直在发生。 - 隔离 Redis DB 15 并非本套件独占:其中仍有他人 7 个键(
auth:refresh:…、auth:user:127:tokens、auth:token:482e6fdc-…、asynq:queues、asynq:{export:*}×3)。因此验证工具默认不执行FlushDB, 只按清单删除自己使用的键;DB 0/6/7 在所有写入路径上被显式拒绝(DB 6/7 仅做过EXISTS/DBSIZE/SCAN只读探测)。 - 成功路径与并发结论不得外推:本轮所有执行都走「能力未配置」分支,因此「零上游」「状态收敛」的结论仅适用于该分支;
成功路径
Complete、真并发互斥、尝试上限收敛见第 4 节未验证清单。
6. 收尾处置
- 验证脚手架
cmd/priority-fixture-tmp/(main.go / verify.go / seam.go / hint.go / purge.go)为一次性工具, 验证完成后整目录删除(rm -rf cmd/priority-fixture-tmp,DELETED_EXIT=0)。 删除确认:ls cmd/只剩api/audit-coverage/audit-retention-simulate/foundation-check/gendocs/migration-finalize/worker;find . -name 'priority-fixture-tmp*' -not -path './.git/*'无输出(仓库根目录与全仓均无同名二进制);git status --porcelain | grep -i fixture无输出(工作区已不含该目录)。 - 测试库
tb_polling_priority_item与本 Change 动作码审计、prio:outbox、标记夹具均已清零(见 2.7); 本文引用的逐场景日志保存在/tmp/aug26-016/(会话级临时目录,非仓库产物)。 - 未提交、未 push。2026-09-17 归档:Change 目录移至
openspec/changes/archive/2026-09-17-add-priority-polling-queue/(含.openspec.yaml),delta spec 已同步进主 Specs(新增openspec/specs/priority-polling-queue/spec.md,并在openspec/specs/polling-operations/spec.md追加「优先轮询项与普通轮询的单次执行互斥」与三条优先队列路由);tasks.md的 9.1–9.7 按用户口径统一勾选,第 4 节未验证清单不因勾选而改变。