feat(轮询优先队列): AUG26-016 卡轮询优先队列、人工入队与读侧接口,归档并同步主 Spec 与证据矩阵
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 14m13s

新增 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。
This commit is contained in:
2026-09-17 14:29:56 +08:00
parent e7b93e4634
commit aab56a6998
59 changed files with 4129 additions and 221 deletions

View File

@@ -0,0 +1,207 @@
# 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.29.7 本机可安全验证部分AF + 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:16299` **DB 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`(仅 SELECTRedis 只读用 `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` | 应用到 228exit=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 插入活动行 Aid=1 card_id=6012 task_type=carddata status=pending
[报错] P2 重复活动行(同卡同任务类型) → SQLSTATE=23505 constraint=uq_polling_priority_item_active
[成功] P3 软删 A 后插入活动行 Bid=3→ 软删行不占用活动项键位
[报错] P4 插入活动行 CB 仍活动) → SQLSTATE=23505 constraint=uq_polling_priority_item_active
[成功] P5 把 B 转终态completed/success后插入活动行 Did=5→ 终态行不占用活动项键位
[报错] P6 终态与结果一致性completed 但 result 为空) → SQLSTATE=23514 constraint=ck_polling_priority_item_terminal_result
索引与约束探针全部符合预期 exit=0
```
### 2.3 AF 批次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_idoutbox 仍只 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_itemorigins 含 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) 租约内 = 跳过且零上游 | `状态/认领时间/尝试次数/失败原因全部不变 → processingclaimed_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
[清理] 优先项=0Outbox(prio:)=0套餐使用=0绑定=3卡=6设备=1
[PASS] 清理后计数 … 应为 09 条)+ 本批全部自建行已清理干净
结果:全部断言通过
```
### 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:37Z02: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. 边界声明(避免误读)
1. **`tb_integration_log` 全表计数会被在跑的测试部署推高**:观测窗口内该表由 5,905,167 增至 5,905,619+452
来源是并发运行的既有测试部署DB 6 心跳/并发/27 卡在跑),**不是本套件产生**;本套件在该表只插入并删除 1 行对照行(复查 = 0
2. **「零上游调用」的判据是夹具卡维度**`tb_integration_log WHERE resource_type='iot_card' AND resource_id='<夹具卡ID>'`
每条场景运行前后均为 015 条独立断言),并由 `counterControl` 对照证明该计数口径能发现写入。
这**不等于**「整库无上游调用」——测试部署的正常上游调用一直在发生。
3. **隔离 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` 只读探测)。
4. **成功路径与并发结论不得外推**:本轮所有执行都走「能力未配置」分支,因此「零上游」「状态收敛」的结论仅适用于该分支;
成功路径 `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.19.7 按用户口径统一勾选,第 4 节未验证清单不因勾选而改变。

View File

@@ -3337,6 +3337,17 @@
"requirements": [],
"classification": "route_index_or_infrastructure"
},
{
"entry_type": "async",
"entry": "constants.OutboxEventTypePollingPriorityRequested",
"capability": "infrastructure",
"requirements": [
"priority-polling-queue::优先轮询触发场景与入队事实",
"priority-polling-queue::入队执行对象为资产当前在用卡",
"priority-polling-queue::同卡同任务类型的唯一活动项与合并"
],
"classification": "route_index_or_infrastructure"
},
{
"entry_type": "async",
"entry": "constants.TaskTypeAgentRechargeRecovery",
@@ -3558,7 +3569,12 @@
"operations-audit::轮询审计事实保留边界",
"polling-load-control::轮询全局并发与背压",
"polling-load-control::正常无变化观测的低写入处理",
"polling-load-control::业务变化与轮询异常仍可追溯"
"polling-load-control::业务变化与轮询异常仍可追溯",
"polling-operations::优先轮询项与普通轮询的单次执行互斥",
"priority-polling-queue::优先调度与执行提示通道",
"priority-polling-queue::优先项认领、租约与执行前校验",
"priority-polling-queue::优先项尝试次数与出队",
"priority-polling-queue::与运营商通道阈值停机的边界"
],
"classification": "behavior"
},
@@ -3572,7 +3588,12 @@
"operations-audit::轮询审计事实保留边界",
"polling-load-control::轮询全局并发与背压",
"polling-load-control::正常无变化观测的低写入处理",
"polling-load-control::业务变化与轮询异常仍可追溯"
"polling-load-control::业务变化与轮询异常仍可追溯",
"polling-operations::优先轮询项与普通轮询的单次执行互斥",
"priority-polling-queue::优先调度与执行提示通道",
"priority-polling-queue::优先项认领、租约与执行前校验",
"priority-polling-queue::优先项尝试次数与出队",
"priority-polling-queue::与运营商通道阈值停机的边界"
],
"classification": "behavior"
},
@@ -3583,7 +3604,12 @@
"requirements": [
"polling-operations::手工轮询任务状态",
"polling-load-control::轮询全局并发与背压",
"polling-operations::停复机遵循实际生效实名策略"
"polling-operations::停复机遵循实际生效实名策略",
"polling-operations::优先轮询项与普通轮询的单次执行互斥",
"priority-polling-queue::优先调度与执行提示通道",
"priority-polling-queue::优先项认领、租约与执行前校验",
"priority-polling-queue::优先项尝试次数与出队",
"priority-polling-queue::与运营商通道阈值停机的边界"
],
"classification": "behavior"
},
@@ -3607,7 +3633,12 @@
"operations-audit::轮询审计事实保留边界",
"polling-load-control::轮询全局并发与背压",
"polling-load-control::正常无变化观测的低写入处理",
"polling-load-control::业务变化与轮询异常仍可追溯"
"polling-load-control::业务变化与轮询异常仍可追溯",
"polling-operations::优先轮询项与普通轮询的单次执行互斥",
"priority-polling-queue::优先调度与执行提示通道",
"priority-polling-queue::优先项认领、租约与执行前校验",
"priority-polling-queue::优先项尝试次数与出队",
"priority-polling-queue::与运营商通道阈值停机的边界"
],
"classification": "behavior"
},
@@ -4375,5 +4406,36 @@
"carrier-channel-traffic-threshold::通道阈值停机与周期恢复"
],
"classification": "behavior"
},
{
"entry_type": "http",
"entry": "POST /api/admin/polling-priority-items",
"capability": "polling-operations",
"requirements": [
"identity-access::数据范围拒绝",
"priority-polling-queue::人工优先入队不受人工触发防滥用配额约束",
"priority-polling-queue::优先轮询事实与查询的可追溯"
],
"classification": "behavior"
},
{
"entry_type": "http",
"entry": "GET /api/admin/polling-priority-items",
"capability": "polling-operations",
"requirements": [
"identity-access::数据范围拒绝",
"priority-polling-queue::优先轮询事实与查询的可追溯"
],
"classification": "behavior"
},
{
"entry_type": "http",
"entry": "GET /api/admin/polling-priority-items/{id}",
"capability": "polling-operations",
"requirements": [
"identity-access::数据范围拒绝",
"priority-polling-queue::优先轮询事实与查询的可追溯"
],
"classification": "behavior"
}
]
]

View File

@@ -4192,5 +4192,311 @@
"验证口径:停复机执行的运营商回执以本机 stub 端点驱动,真实运营商成功回执路径未端到端验证"
]
}
},
{
"capability": "priority-polling-queue",
"requirement": "优先轮询触发场景与入队事实",
"spec": "openspec/specs/priority-polling-queue/spec.md",
"entries": [
"/api/admin/polling-priority-items",
"constants.OutboxEventTypePollingPriorityRequested"
],
"handler_consumer_job": [
"internal/service/package/priority.go:AppendActivatedPriorityRequested / AppendNoValidPackagePriorityRequested",
"internal/infrastructure/prioritypolling/event.go:PriorityEventWriter.AppendPriorityRequested",
"internal/infrastructure/prioritypolling/event.go:PriorityRequestedConsumer.Consume"
],
"application_service_query": [
"internal/service/order/service.go:AppendActivatedPriorityRequested(...)(主套餐购买生效分支)",
"internal/service/package/activation_service.go:AppendActivatedPriorityRequested(...)(排队顺延生效)",
"internal/task/auto_purchase.go:AppendActivatedPriorityRequested(...)",
"internal/service/iot_card/stop_resume_service.go:AppendNoValidPackagePriorityRequested(...)"
],
"domain_state_amount": [
"pkg/constants/polling.go:PollingPriorityTriggerPurchaseActivated/RenewalActivated/QueueActivated/AddonActivated/NoValidPackage/Manual",
"internal/model/polling.go:PollingPriorityItem.TriggerType/TriggerTypes/TriggerCount"
],
"store_migration_config": [
"migrations/000228_add_polling_priority_queue.up.sql",
"internal/store/postgres/polling_priority_item_store.go:InsertOrMerge"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"pkg/constants/polling.go\", \"PollingPriorityTriggerPurchaseActivated = \\\"purchase_activated\\\"\"), (\"pkg/constants/polling.go\", \"PollingPriorityTriggerRenewalActivated = \\\"renewal_activated\\\"\"), (\"pkg/constants/polling.go\", \"PollingPriorityTriggerQueueActivated = \\\"queue_activated\\\"\"), (\"pkg/constants/polling.go\", \"PollingPriorityTriggerAddonActivated = \\\"addon_activated\\\"\"), (\"pkg/constants/polling.go\", \"PollingPriorityTriggerNoValidPackage = \\\"no_valid_package\\\"\"), (\"pkg/constants/polling.go\", \"PollingPriorityTriggerManual = \\\"manual_trigger\\\"\"), (\"internal/service/package/priority.go\", \"func ResolveActivationTriggerType(\"), (\"internal/service/package/priority.go\", \"func HasEarlierPackageUsage(\"), (\"internal/service/order/service.go\", \"AppendActivatedPriorityRequested(ctx, tx, s.priorityEvents\"), (\"internal/service/package/activation_service.go\", \"AppendActivatedPriorityRequested(ctx, tx, s.priorityEvents\"), (\"internal/task/auto_purchase.go\", \"packagepkg.AppendActivatedPriorityRequested(\"), (\"internal/service/iot_card/stop_resume_service.go\", \"packagepkg.AppendNoValidPackagePriorityRequested(\"), (\"internal/infrastructure/prioritypolling/event.go\", \"func (w *PriorityEventWriter) AppendPriorityRequested(\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: priority-polling-queue::优先轮询触发场景与入队事实\")'",
"literal_output": [
"OK: priority-polling-queue::优先轮询触发场景与入队事实"
],
"exit_status": 0
}
},
{
"capability": "priority-polling-queue",
"requirement": "入队执行对象为资产当前在用卡",
"spec": "openspec/specs/priority-polling-queue/spec.md",
"entries": [
"constants.OutboxEventTypePollingPriorityRequested"
],
"handler_consumer_job": [
"internal/infrastructure/prioritypolling/event.go:PriorityRequestedConsumer.Consume逐卡 × 逐任务类型建项)"
],
"application_service_query": [
"internal/service/package/priority.go:filterPackageUsageByCarrier载体口径与卡的既有有效套餐判定一致"
],
"domain_state_amount": [
"internal/application/prioritypolling/event.go:PriorityRequestedEvent.ResourceIDs触发事务内冻结的绑定卡快照",
"internal/model/polling.go:PollingPriorityItem.CardID/ShopIDSnapshot"
],
"store_migration_config": [
"migrations/000228_add_polling_priority_queue.up.sql:tb_polling_priority_item.card_id 列注释「执行对象恒为卡,多卡资产逐卡建项」"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"internal/application/prioritypolling/event.go\", \"ResourceIDs\"), (\"internal/infrastructure/prioritypolling/event.go\", \"cardIDs = event.ResourceIDs\"), (\"internal/infrastructure/prioritypolling/event.go\", \"taskTypes := constants.PollingPriorityTaskTypes()\"), (\"pkg/constants/polling.go\", \"func PollingPriorityTaskTypes()\"), (\"internal/infrastructure/prioritypolling/event.go\", \"loadCardShopSnapshots\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) ListActiveByCards(\"), (\"internal/infrastructure/prioritypolling/event.go\", \"!CurrentSlotNo\"), (\"internal/application/prioritypolling/event.go\", \"!CurrentSlotNo\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: priority-polling-queue::入队执行对象为资产当前在用卡\")'",
"literal_output": [
"OK: priority-polling-queue::入队执行对象为资产当前在用卡"
],
"exit_status": 0
}
},
{
"capability": "priority-polling-queue",
"requirement": "同卡同任务类型的唯一活动项与合并",
"spec": "openspec/specs/priority-polling-queue/spec.md",
"entries": [
"constants.OutboxEventTypePollingPriorityRequested"
],
"handler_consumer_job": [
"internal/infrastructure/prioritypolling/event.go:PriorityRequestedConsumer.Consume重复投递只合并不新建行"
],
"application_service_query": [
"internal/service/polling/priority_enqueue_service.go:Enqueue人工侧同一 InsertOrMerge 语义)"
],
"domain_state_amount": [
"internal/model/polling.go:PollingPriorityItem.Status/TriggerTypes/TriggerCount/LastTriggeredAt"
],
"store_migration_config": [
"migrations/000228_add_polling_priority_queue.up.sql:CREATE UNIQUE INDEX uq_polling_priority_item_active ... WHERE deleted_at IS NULL AND status IN ('pending','processing')",
"internal/store/postgres/polling_priority_item_store.go:InsertOrMerge/mergeActive/isPollingPriorityActiveConflict"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"migrations/000228_add_polling_priority_queue.up.sql\", \"CREATE UNIQUE INDEX uq_polling_priority_item_active\"), (\"migrations/000228_add_polling_priority_queue.up.sql\", \"活动项唯一键必须是「部分唯一索引」\"), (\"migrations/000228_add_polling_priority_queue.up.sql\", \"WHERE deleted_at IS NULL AND status IN (\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) InsertOrMerge(\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) mergeActive(\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"pgErr.ConstraintName == pollingPriorityActiveConstraint\"), (\"internal/infrastructure/prioritypolling/event.go\", \"created, insertErr := store.InsertOrMerge(ctx, item)\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: priority-polling-queue::同卡同任务类型的唯一活动项与合并\")'",
"literal_output": [
"OK: priority-polling-queue::同卡同任务类型的唯一活动项与合并"
],
"exit_status": 0
}
},
{
"capability": "priority-polling-queue",
"requirement": "优先调度与执行提示通道",
"spec": "openspec/specs/priority-polling-queue/spec.md",
"entries": [
"constants.TaskTypePollingRealname",
"constants.TaskTypePollingCarddata",
"constants.TaskTypePollingCardStatus",
"constants.TaskTypePollingPackage"
],
"handler_consumer_job": [
"internal/polling/queue_manager.go:PollingQueueManager.EnqueuePriority",
"cmd/worker/main.go:registerPriorityPollingOutboxConsumer"
],
"application_service_query": [
"internal/application/prioritypolling/event.go:PromptPublisher.EnqueuePriority提示不是权威失败只退化为延迟一个周期"
],
"domain_state_amount": [
"pkg/constants/redis.go:RedisPollingPriorityQueueKey每任务类型独立键不复用手动触发队列键"
],
"store_migration_config": [
"internal/polling/scheduler.go:Scheduler.processPriorityQueue同一调度周期先排空优先提示、再排空手动触发队列且不参与分片背压判断"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"internal/polling/queue_manager.go\", \"func (m *PollingQueueManager) EnqueuePriority(\"), (\"pkg/constants/redis.go\", \"func RedisPollingPriorityQueueKey(taskType string) string\"), (\"internal/polling/scheduler.go\", \"func (s *Scheduler) processShardSchedule(ctx context.Context)\"), (\"internal/polling/scheduler.go\", \"func (s *Scheduler) processPriorityQueue(\"), (\"internal/polling/scheduler.go\", \"s.processPriorityQueue(ctx, taskType, s.cfg.MaxManualBatchSize)\"), (\"internal/application/prioritypolling/event.go\", \"type PromptPublisher interface\"), (\"cmd/worker/main.go\", \"registerPriorityPollingOutboxConsumer\"), (\"pkg/constants/polling.go\", \"!TaskTypePollingPriority\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: priority-polling-queue::优先调度与执行提示通道\")'",
"literal_output": [
"OK: priority-polling-queue::优先调度与执行提示通道"
],
"exit_status": 0
}
},
{
"capability": "priority-polling-queue",
"requirement": "优先项认领、租约与执行前校验",
"spec": "openspec/specs/priority-polling-queue/spec.md",
"entries": [
"constants.TaskTypePollingRealname",
"constants.TaskTypePollingCarddata",
"constants.TaskTypePollingCardStatus",
"constants.TaskTypePollingPackage"
],
"handler_consumer_job": [
"internal/task/polling_priority_claim.go:PollingBase.beginPollingPriority接缝入口",
"internal/task/polling_realname_handler.go / polling_carddata_handler.go / polling_cardstatus_handler.go / polling_package_handler.go:Handle 调用 beginPollingPriority"
],
"application_service_query": [
"internal/task/polling_priority_claim.go:pollingPriorityRound + priorityInPollingScope执行前校验卡自身与绑定设备的轮询开关"
],
"domain_state_amount": [
"internal/model/polling.go:PollingPriorityItem.Status/ClaimedAt",
"pkg/constants/polling.go:PollingPriorityClaimLease = 90"
],
"store_migration_config": [
"internal/store/postgres/polling_priority_item_store.go:ClaimPending/TakeOverExpiredClaim"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"pkg/constants/polling.go\", \"const PollingPriorityClaimLease = 90\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) ClaimPending(\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) TakeOverExpiredClaim(\"), (\"internal/task/polling_priority_claim.go\", \"func (b *PollingBase) beginPollingPriority(\"), (\"internal/task/polling_priority_claim.go\", \"func (b *PollingBase) priorityInPollingScope(\"), (\"internal/task/polling_realname_handler.go\", \"h.base.beginPollingPriority(ctx, cardID, constants.TaskTypePollingRealname)\"), (\"internal/task/polling_carddata_handler.go\", \"h.base.beginPollingPriority(ctx, cardID, constants.TaskTypePollingCarddata)\"), (\"internal/task/polling_cardstatus_handler.go\", \"h.base.beginPollingPriority(ctx, cardID, constants.TaskTypePollingCardStatus)\"), (\"internal/task/polling_package_handler.go\", \"h.base.beginPollingPriority(ctx, cardID, constants.TaskTypePollingPackage)\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: priority-polling-queue::优先项认领、租约与执行前校验\")'",
"literal_output": [
"OK: priority-polling-queue::优先项认领、租约与执行前校验"
],
"exit_status": 0
}
},
{
"capability": "priority-polling-queue",
"requirement": "优先项尝试次数与出队",
"spec": "openspec/specs/priority-polling-queue/spec.md",
"entries": [
"constants.TaskTypePollingRealname",
"constants.TaskTypePollingCarddata",
"constants.TaskTypePollingCardStatus",
"constants.TaskTypePollingPackage"
],
"handler_consumer_job": [
"internal/task/polling_priority_claim.go:pollingPriorityRound.MarkExecution/FailRetryable/FailFinal/Complete"
],
"application_service_query": [
"internal/task/polling_priority_claim.go:pollingPriorityRound.FailRetryable与 PollingPriorityMaxAttempts 比较后决定回活动态或失败出队)"
],
"domain_state_amount": [
"internal/model/polling.go:PollingPriorityItem.AttemptCount/FailureReason/Status/Result",
"pkg/constants/polling.go:PollingPriorityMaxAttempts = 3"
],
"store_migration_config": [
"internal/store/postgres/polling_priority_item_store.go:IncrementAttempt/ReturnToPending/MarkFailed/MarkCompleted"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"pkg/constants/polling.go\", \"const PollingPriorityMaxAttempts = 3\"), (\"internal/task/polling_priority_claim.go\", \"if r.attempts < constants.PollingPriorityMaxAttempts {\"), (\"internal/task/polling_priority_claim.go\", \"func (r *pollingPriorityRound) FailRetryable(\"), (\"internal/task/polling_priority_claim.go\", \"func (r *pollingPriorityRound) FailFinal(\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) IncrementAttempt(\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) ReturnToPending(\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) MarkFailed(\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) MarkCompleted(\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: priority-polling-queue::优先项尝试次数与出队\")'",
"literal_output": [
"OK: priority-polling-queue::优先项尝试次数与出队"
],
"exit_status": 0
}
},
{
"capability": "priority-polling-queue",
"requirement": "优先轮询事实与查询的可追溯",
"spec": "openspec/specs/priority-polling-queue/spec.md",
"entries": [
"/api/admin/polling-priority-items",
"/api/admin/polling-priority-items/{id}"
],
"handler_consumer_job": [
"internal/handler/admin/polling_priority.go:PriorityPollingHandler.Enqueue/ListPriorityItems/GetPriorityItem",
"internal/routes/polling_priority.go:registerPollingPriorityRoutes"
],
"application_service_query": [
"internal/query/prioritypolling/query.go:Query.List/Detail + priorityPollingShopScope数据权限下推"
],
"domain_state_amount": [
"pkg/constants/audit.go:AuditActionPollingPriority{Enqueued,Claimed,Failed,Retried,Completed,Dequeued,ManualDenied}",
"pkg/constants/audit.go:AuditResourcePollingPriorityItem"
],
"store_migration_config": [
"internal/store/postgres/polling_priority_item_store.go:List/GetByIDScoped/applyPollingPriorityShopScope"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"pkg/constants/audit.go\", \"AuditActionPollingPriorityEnqueued = \\\"polling_priority.enqueue\\\"\"), (\"pkg/constants/audit.go\", \"AuditActionPollingPriorityClaimed = \\\"polling_priority.claim\\\"\"), (\"pkg/constants/audit.go\", \"AuditActionPollingPriorityFailed = \\\"polling_priority.fail\\\"\"), (\"pkg/constants/audit.go\", \"AuditActionPollingPriorityRetried = \\\"polling_priority.retry\\\"\"), (\"pkg/constants/audit.go\", \"AuditActionPollingPriorityCompleted = \\\"polling_priority.complete\\\"\"), (\"pkg/constants/audit.go\", \"AuditActionPollingPriorityDequeued = \\\"polling_priority.dequeue\\\"\"), (\"pkg/constants/audit.go\", \"AuditActionPollingPriorityManualDenied = \\\"polling_priority.manual_denied\\\"\"), (\"internal/query/prioritypolling/query.go\", \"func (q *Query) List(ctx context.Context, req dto.PriorityPollingItemListRequest)\"), (\"internal/query/prioritypolling/query.go\", \"func (q *Query) Detail(ctx context.Context, id uint)\"), (\"internal/query/prioritypolling/query.go\", \"func priorityPollingShopScope(ctx context.Context)\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) GetByIDScoped(\"), (\"internal/handler/admin/polling_priority.go\", \"func (h *PriorityPollingHandler) Enqueue(c *fiber.Ctx) error\"), (\"internal/handler/admin/polling_priority.go\", \"func (h *PriorityPollingHandler) ListPriorityItems(c *fiber.Ctx) error\"), (\"internal/handler/admin/polling_priority.go\", \"func (h *PriorityPollingHandler) GetPriorityItem(c *fiber.Ctx) error\"), (\"internal/routes/polling_priority.go\", \"router.Group(\\\"/polling-priority-items\\\")\"), (\"internal/model/dto/polling_priority_dto.go\", \"!PriorityLevel\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: priority-polling-queue::优先轮询事实与查询的可追溯\")'",
"literal_output": [
"OK: priority-polling-queue::优先轮询事实与查询的可追溯"
],
"exit_status": 0
}
},
{
"capability": "priority-polling-queue",
"requirement": "人工优先入队不受人工触发防滥用配额约束",
"spec": "openspec/specs/priority-polling-queue/spec.md",
"entries": [
"/api/admin/polling-priority-items"
],
"handler_consumer_job": [
"internal/handler/admin/polling_priority.go:PriorityPollingHandler.Enqueue"
],
"application_service_query": [
"internal/service/polling/priority_enqueue_service.go:PriorityEnqueueService.Enqueue",
"internal/service/polling/permission.go:canManagePollingCard与既有人工触发共用同一权限判定"
],
"domain_state_amount": [
"internal/model/polling.go:PollingPriorityItem.ManualReason/ManualOperatorID/ManualOperatorName",
"pkg/constants/polling.go:PollingPriorityManualReasonMaxLength = 500"
],
"store_migration_config": [
"migrations/000228_add_polling_priority_queue.up.sql:manual_reason 列注释「人工入队必填,与事实一同保存」"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"internal/service/polling/priority_enqueue_service.go\", \"func (s *PriorityEnqueueService) Enqueue(\"), (\"internal/service/polling/priority_enqueue_service.go\", \"constants.PollingPriorityTriggerManual\"), (\"internal/service/polling/priority_enqueue_service.go\", \"人工优先入队原因不能为空\"), (\"internal/service/polling/priority_enqueue_service.go\", \"!已达到每日触发次数上限\"), (\"internal/service/polling/priority_enqueue_service.go\", \"!RedisPollingManualDedupeKey\"), (\"internal/service/polling/priority_enqueue_service.go\", \"canManagePollingCard(ctx, s.iotCardStore, cardID)\"), (\"internal/service/polling/priority_enqueue_service.go\", \"AuditActionPollingPriorityEnqueued\"), (\"internal/service/polling/permission.go\", \"func canManagePollingCard(\"), (\"internal/service/polling/manual_trigger_service.go\", \"已达到每日触发次数上限\"), (\"internal/service/polling/manual_trigger_service.go\", \"RedisPollingManualDedupeKey\"), (\"pkg/constants/polling.go\", \"const PollingPriorityManualReasonMaxLength = 500\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: priority-polling-queue::人工优先入队不受人工触发防滥用配额约束\")'",
"literal_output": [
"OK: priority-polling-queue::人工优先入队不受人工触发防滥用配额约束"
],
"exit_status": 0
}
},
{
"capability": "priority-polling-queue",
"requirement": "与运营商通道阈值停机的边界",
"spec": "openspec/specs/priority-polling-queue/spec.md",
"entries": [
"constants.TaskTypePollingRealname",
"constants.TaskTypePollingCarddata",
"constants.TaskTypePollingCardStatus",
"constants.TaskTypePollingPackage"
],
"handler_consumer_job": [
"internal/task/polling_priority_claim.go:PollingBase.priorityInPollingScope优先轮次仍走既有轮询范围校验"
],
"application_service_query": [
"internal/service/iot_card/channel_threshold.go:StopResumeService.channelThresholdBlocked持锁拒绝复机优先执行不绕过",
"internal/service/iot_card/stop_resume_service.go:5 处自动/手动/强制复机入口调用 channelThresholdBlocked"
],
"domain_state_amount": [
"internal/service/iot_card/stop_resume_service.go:ChannelThresholdLockGuard可选的通道阈值持锁判定能力"
],
"store_migration_config": [
"internal/service/iot_card/channel_threshold.go:recordChannelThresholdDenial保留既有持锁拒绝事实"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"internal/service/iot_card/channel_threshold.go\", \"func (s *StopResumeService) channelThresholdBlocked(ctx context.Context, card *model.IotCard) (bool, error)\"), (\"internal/service/iot_card/channel_threshold.go\", \"func (s *StopResumeService) recordChannelThresholdDenial(ctx context.Context, card *model.IotCard, actionCode, summary string)\"), (\"internal/service/iot_card/channel_threshold.go\", \"func channelThresholdDenyError() error\"), (\"internal/service/iot_card/stop_resume_service.go\", \"blocked, err := s.channelThresholdBlocked(ctx, card)\"), (\"internal/service/iot_card/stop_resume_service.go\", \"channelThresholdDenyError()\"), (\"internal/task/polling_priority_claim.go\", \"func (b *PollingBase) priorityInPollingScope(\"), (\"internal/task/polling_priority_claim.go\", \"!channelThreshold\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: priority-polling-queue::与运营商通道阈值停机的边界\")'",
"literal_output": [
"OK: priority-polling-queue::与运营商通道阈值停机的边界"
],
"exit_status": 0
}
},
{
"capability": "polling-operations",
"requirement": "优先轮询项与普通轮询的单次执行互斥",
"spec": "openspec/specs/polling-operations/spec.md",
"entries": [
"constants.TaskTypePollingRealname",
"constants.TaskTypePollingCarddata",
"constants.TaskTypePollingCardStatus",
"constants.TaskTypePollingPackage"
],
"handler_consumer_job": [
"internal/task/polling_priority_claim.go:PollingBase.beginPollingPriority无活动项返回跳过标志 false行为完全等价既有路径",
"四个轮询 HandlerbeginPollingPriority 返回 skip 时按既有间隔延后,不发起上游调用"
],
"application_service_query": [
"internal/task/polling_priority_claim.go:newPriorityRound认领成功即作为本轮执行完成不产生第二次调用"
],
"domain_state_amount": [
"pkg/constants/polling.go:PollingPriorityStatusPending/Processing活动集合"
],
"store_migration_config": [
"internal/store/postgres/polling_priority_item_store.go:FindActive/ClaimPending/TakeOverExpiredClaim"
],
"verification": {
"command": "python3 -c 'from pathlib import Path; checks=[(\"internal/task/polling_priority_claim.go\", \"if b == nil || b.priorityStore == nil\"), (\"internal/task/polling_priority_claim.go\", \"case constants.PollingPriorityStatusPending:\"), (\"internal/task/polling_priority_claim.go\", \"case constants.PollingPriorityStatusProcessing:\"), (\"internal/task/polling_priority_claim.go\", \"now.Before(item.ClaimedAt.Add(time.Duration(constants.PollingPriorityClaimLease)*time.Second))\"), (\"internal/task/polling_priority_claim.go\", \"func (b *PollingBase) newPriorityRound(\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) FindActive(\"), (\"internal/store/postgres/polling_priority_item_store.go\", \"func (s *PollingPriorityItemStore) ClaimPending(\")]; miss=[p+\" :: \"+t for p,t in checks if ((t[1:] if t.startswith(\"!\") else t) in Path(p).read_text()) != (not t.startswith(\"!\"))]; print(\"MISSING: \"+\" | \".join(miss)) if miss else print(\"OK: polling-operations::优先轮询项与普通轮询的单次执行互斥\")'",
"literal_output": [
"OK: polling-operations::优先轮询项与普通轮询的单次执行互斥"
],
"exit_status": 0
}
}
]
]