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,2 @@
schema: spec-driven
created: 2026-08-31

View File

@@ -0,0 +1,168 @@
## Context
现状(已核实):
- 执行通道16 分片 ZSetscore=下次检查时间)→ 调度器每 1 秒先排空各任务类型的手动队列List `polling:manual:{taskType}`),再扫描分片 → 既有 Asynq 队列5 类任务 realname/carddata/package/protect/card_status→ 既有轮询 Handler。手动队列排空不受分片背压阈值约束分片批次会被背压跳过。
- 并发:分类型 + 全局两个 Redis Lua 信号量,上限来自 `tb_polling_concurrency_config`(上限 1000既有任务 `MaxRetry(0)``Timeout(60s)`
- 失败Handler 内部按配置间隔无限重入队,无失败次数上限、无补偿判据。
- 卡级锁只有流量同步锁,仅服务 carddata、人工刷新与观测序列不能作为通用互斥。
- 触发事实现状:主套餐立即生效、排队主套餐顺延生效、加油包生效三处已在同一业务事务写卡观测序列事件,但载荷不含触发场景;资产无有效套餐只在停复机评估内部判定;普通轮询失败没有任何可判定条件。
- 观测序列通道是另一套执行路径(阶梯尝试),对同一资源有各自的并发保护。
需求权威为 PRD §2.15优先轮询定位111.md §23 仅作追溯,其优先级分级、优先级规则配置、有效期与异常记录页均不在本 Change 范围。本设计只解释实现方式,行为契约以 specs 为准。
## Goals / Non-Goals
**Goals:**
- 让命中场景的资产在不改变普通轮询内容的前提下额外获得一次优先执行的既有轮询。
- 加急事实可持久化、可审计、可查询,且人工加急与既有人工触发权限语义一致。
- 复用既有调度、既有任务类型、既有并发与重试保护,不引入第二套轮询逻辑。
**Non-Goals:**
- 不做 P0/P1/P2 分级与优先级规则配置;不引入有效期字段与过期语义。
- 不做独立异常记录页与人工重触发按钮(重触发走既有人工触发或人工优先入队)。
- 不新增可维护的参数配置项;不改动普通轮询内容、并发上限、失败重试与外部调用保护。
- 不新建调度设施、Asynq 任务类型或队列;不建通用卡级锁;不改分片队列的出队、入队与移除路径。
- 不改人工触发 API 的任务类型白名单、每日次数上限与 24 小时去重;不改通道阈值停机锁的复机拒绝语义。
## Decisions
### D1 队列载体与执行提示通道分工
持久化事实与认领权威是新增优先项表;执行提示走**独立**提示通道键(每任务类型一条 List不复用 `polling:manual:{taskType}`。调度器在**同一调度周期内先排空优先提示、再排空手动触发队列**,其余调度路径不变。
- 替代方案一:新建独立调度器/独立 Asynq 队列。放弃PRD 明确「仅调度顺序优先,不另建参数配置」,且新队列需要新增权重与 Worker 配置。
- 替代方案二:复用人工触发队列。放弃,人工触发的每秒排空额度有限,大批自动触发会把人工触发挤到队列尾部,且两类提示无法区分观测。
- 强度边界(须写进实现与验收):优先只在调度入口领先——跳过分片 ZSet 的定时等待,并在同一周期内先入队;同一任务类型的既有 Asynq 队列内不插队;分片背压跳过不影响优先提示入队。这正是 PRD「仅调度顺序优先」的范围。
### D2 唯一活动项粒度 = (card_id, task_type)
同一卡同一轮询任务类型至多一条活动项;一次优先需求对应多个既有任务类型的执行。
- 理由:一次加急天然要对多个任务类型各执行一次,单行 + 多类型 JSON 会同时削弱认领的原子性与逐类型的执行结果审计;按类型分行的互斥语义精确,避免「整卡跳过」饿死其它类型的普通轮询,与 PRD「不移除、暂停或改变普通轮询」一致。
- 合并语义不变:后续触发合并进已存在的活动行,追加触发次数、最近触发时间与来源,不新建行、不重复调用。
- 替代方案:单行 + 类型集合 JSON。放弃认领需要 JSON 级原子更新,逐类型失败原因与结果无法独立审计。
- 执行集合取 `{realname, carddata, package, card_status}`:对应 111.md 轮询目标枚举protect保护期一致性不纳入它是设备维度的一致性修正且会主动发起强制停复机与五类触发场景无关。
### D3 互斥实现:条件认领,而非通用锁或 Redis 活动标记
在轮询共享基类新增接缝,位于**取并发信号量之后、调用上游之前**,四个纳入集合的 Handler 接入:查询该卡该任务类型的活动项(部分唯一索引探测)。
- 无活动项 → 完全等价既有行为(普通轮询入队、出队、移除路径一行不改)。
- 待执行 → 条件更新(待执行→执行中)命中者即「领取成功」,本轮执行同时就是优先执行;未命中(已被他人领取)→ 跳过该轮次并按既有间隔延后。
- 执行中且租约未到期 → 跳过并延后,不发第二次上游调用。
- 并发安全由数据库条件更新提供,不需要 Redis 活动标记,也不需要通用卡级锁(既有卡级锁只服务流量同步链,语义不匹配)。分片队列的出队、入队与移除路径保持原样。
- 关键澄清:**「存在活动项就跳过」会饿死队列**——待执行项必须由到达的执行认领并执行,否则提示通道产生的执行也会被自己跳过,优先项永远无人执行。可观察契约因此区分为「待执行=认领并执行」与「执行中=跳过并延后」。
### D4 认领租约与崩溃接管
执行中项带认领租约 90 秒(大于既有任务超时 60 秒,留出收尾与重入队余量)。超过租约仍未被更新的执行中项允许相邻执行接管领取。
- 理由:没有租约时,崩溃遗留的「执行中」会让该卡该任务类型被永久跳过,等价于悄悄停掉该卡该类型的轮询——这是不可接受的静默退化。
- 租约接管不违反「不重复调用」:崩溃的执行没有产生有效结果,重新执行是必要恢复;实际并发仍受既有信号量约束。
### D5 提示通道丢失的兜底
提示通道不是权威:丢失或缓存服务重启时,活动项仍在库中,后续普通轮询执行按 D3 条件认领仍会执行,只退化为延迟一个普通轮询周期。不引入第二套补偿任务或扫描。
### D6 执行前轮询范围校验
执行前校验卡仍在轮询范围内(卡自身与绑定设备的轮询开关均为启用;独立卡命中风险状态会把轮询开关置为关闭)。不满足时终止该项、保留可安全展示的原因,且不发起上游调用。
- 理由:优先通道不得重启已停止轮询的卡;否则加急入口会成为绕过轮询管控的后门。
- 既有人工触发的 3 个入口行为不变,本 Change 不改变其范围语义。
### D7 尝试上限常量与出队
优先项使用自身固定的最大尝试次数常量3不作为可维护配置项普通轮询的无限重试策略不变。
- 尝试次数只在真正发起执行后累加;可恢复失败未达上限回到活动态并按既有间隔重排;达上限记录失败终态与安全原因后出队。
- 业务校验类失败(卡不存在、不在轮询范围)直接失败不重试。
- 出队后该卡继续普通轮询且既有无限重入队仍在,不丢卡。
- 不新增有效期字段、不建独立异常记录表。
### D8 触发事件类型与抑制开关
新增独立可靠事件类型承载「优先轮询请求」,在原业务事务内追加,由 Outbox 在提交后投递;消费者据载荷逐卡建项并下发提示。
- 不能复用卡观测序列事件类型:同一事件类型不允许注册第二个消费者,且该事件驱动的是另一套观测序列通道。
- 载荷沿用既有形状(资源类型/资源 ID/资源 ID 列表),并在事务内冻结绑定卡快照;另带触发类型与来源订单/套餐使用记录标识。
- 抑制开关:既是观测结果驱动的业务评估,写优先请求事件时必须与既有观测序列写入同样短路,避免「观测→业务→优先轮询→观测」的反向触发环。
- 人工入队不经过事件通道,直接在入队用例中写事实并下发提示。
### D9 触发类型枚举与续购判定
稳定枚举:`purchase_activated``renewal_activated``queue_activated``addon_activated``no_valid_package``manual_trigger`。生产者按自身代码路径写入。
- 新购与续购的判定:在生效事务内对同一载体做一次更早套餐使用记录的存在性查询,存在即 `renewal_activated`,否则 `purchase_activated`
- 替代方案:消费者按套餐使用记录现场判定。放弃,消费者无法可靠区分新购与续购(需要历史查询),且会把业务语义判断搬到 Worker。
### D10 无有效套餐的前置条件
该场景 MUST 附带前置条件「该载体存在待生效的套餐使用记录」。
- 理由:不加前置条件时,每个普通套餐轮询周期都会重新命中该场景并触发一轮优先,形成自激循环;前置条件让触发在有真实待同步业务时发生,业务恢复后自然停止。
- 不含「待支付订单」:支付结果未确认时不入队,与既有支付成功才生效的隔离一致。
### D11 多卡资产口径
按资产解析绑定状态有效的全部在用卡(独立卡为自身),在触发事务内冻结快照,逐卡建项、逐卡记录结果;不使用设备上报的当前卡槽作为入队口径。与既有「业务成功时冻结设备绑定卡快照」的口径一致。
### D12 人工入队
复用既有人工触发的权限判定语义(超管/平台全量放行;代理限自身与下级店铺,平台卡不可见;企业拒绝),把该判定抽为同包共享函数,判定语义零变化,不新建角色守卫。入队原因必填并与事实一同保存。
- 不继承每日次数上限与 24 小时去重:它们是人工触发的防滥用配额,不是队列不变量;重复抑制由活动项合并承担,且每次人工入队独立审计。
- 人工入队不改调度优先级、不绕过并发上限。
### D13 读侧最小接口与数据范围下推
提供列表与详情(筛选、分页)。事实表冗余卡所属店铺快照(平台卡为空),查询按既有数据范围下推判定,代理看不到平台卡与范围外卡,越权与不存在响应不可区分。
- 该接口是最小可读性能力,不是 111.md 的「异常与记录」页:不含优先级分级、有效期、人工重触发入口。
### D14 审计归属
- Outbox触发事件业务事务内与既有事件类型不变。
- Audit Event入队含合并、领取、每次失败、重试、终态出队以及人工入队与被拒绝的人工请求人工侧复用既有轮询审计输入与失败短事务模式系统侧使用既有 Worker 审计上下文,并按(操作者类型,来源)补齐注册。
- Integration Log沿用既有「仅变化才记录外部交互」的规则本 Change 不新增。
- Domain LedgerN/A不涉及资金与库存
- 低写入约定保持:优先项生命周期事件低频且属业务事实,不改变「无变化观测不写审计」的既有规则。
### D15 与通道阈值停机的边界
通道阈值停机锁只拒绝复机不使卡退出轮询范围既有移除路径仅由卡状态变化、禁用、删除与设备级轮询关闭触发。优先执行复用既有停复机判定因此持锁拒绝语义天然保留spec 中以可观察断言固化,实现不得绕过停机锁。
## Risks / Trade-offs
- 优先提示不受分片背压约束 → 大批触发时 Asynq 同类型队列会积压。缓解:实际并发仍由既有信号量兜住,积压由既有队列深度监控观测;不新增背压分支(新增分支会在过载时正好压制加急)。
- 每次轮询执行多一次活动项索引探测 → 无匹配时只是索引探测、不产生行锁,成本可控;若日后成为瓶颈,可加「活动卡集合」提示缓存作为纯优化,本版不做(避免第二种可能失真的状态)。
- 优先项存续期间该卡该任务类型的普通执行被延后 → 由尝试上限与认领租约双重兜底,不会无限期;出队后立即恢复普通轮询。
- 多卡资产触发放大(每卡 × 每任务类型)→ 由活动项合并抑制重复,逐卡逐类型记录结果,符合产品口径。
- 无有效套餐场景在「待同步业务持续存在」时每个普通套餐周期触发一轮优先 → 属设计取舍(即加急直到资产恢复);恢复或待同步业务消失后自然停止。此口径须在实施说明中保留,避免被当成缺陷。
- 提示通道与库内事实短期不一致(提示存在但项已终态)→ 到达的执行按事实判定,最多等价一次普通轮询,不产生并发重复调用。
- 观测序列通道与优先轮询可能对同一卡同一时刻发起上游调用(流量同步有卡级锁覆盖;网络/实名观测的间隔保护轮询侧不读取)→ 属既有通道间的既有限制,本 Change 不修改,登记为已知边界。
## Migration Plan
1. 新增成对迁移:优先项表(活动项部分唯一索引 = 卡 + 任务类型,仅覆盖待执行/执行中且未软删;状态与时间索引)+ down 守卫(存在活动项即拒绝回滚,避免静默丢失加急事实)。
2. 模型与存储:关联只存 ID 并显式查询,不新增数据库外键与关联标签。
3. 无数据回填;上线后无活动项时系统行为与当前完全一致,可先发布再依赖触发。
4. 隔离环境执行 up / down / up 验证;回滚前必须确认无活动项,否则按守卫拒绝并人工确认。
5. 发布顺序:先迁移与 Worker消费者、接缝、提示通道再开放人工入队与读侧接口。
## 后续候选
### 普通轮询异常补偿(本版不做)
- 现状无任何可判定条件:轮询失败按配置间隔无限重入队,无失败上限、无补偿入口,因此当前 spec 中的「既有异常补偿条件」是不可实现、不可验证的空指向,已从 spec 与任务中移除。
- 本版若实现等于自造判据(例如「任一次可恢复失败即入队」);且渠道故障时全量失败卡会涌入优先通道,而提示通道不受分片背压约束,风险不对称。
- 处置:需产品确认后以**独立 Change** 实施;届时在 PRD §2.15 该场景处解除本版标注,并同时定义判据、上限与过载保护。
### 其它候选(均需独立 Change
- 优先级分级与优先级规则配置111.md §23.6)。
- 优先项有效期与过期出队111.md §23.4)。
- 独立异常与重试记录页、人工重触发入口、资产/卡详情优先轮询状态展示111.md §23.2、§23.7、§23.11)。

View File

@@ -0,0 +1,43 @@
## Scope
- 迭代编号:`AUG26-016`
## Why
紧急卡状态查询与普通轮询竞争,无法保证处理顺序或追踪加急事实。
## What Changes
- 新增受限管理的卡轮询优先队列:以持久化事实记录触发来源、次数、最近时间与执行结果;同一卡同一轮询任务类型至多一条活动优先项。
- 触发来源:主套餐购买后立即生效、排队主套餐顺延生效、加油包生效、资产无有效套餐且存在待生效套餐使用记录、具备既有人工触发权限的账号人工入队。
- Worker 复用既有轮询任务类型与既有轮询内容优先领取执行;同一卡同一任务类型只允许一次上游调用,优先项不以任何方式使卡退出普通轮询。
- 复用既有手动队列式的执行提示通道(独立提示键,调度周期内先排空),不新建调度设施、异步任务类型或队列。
- 新增最小可读接口(列表与详情,支持筛选与分页),权限与人工入队一致并按数据范围下推。
## 非目标
- 不做优先级分级P0/P1/P2与优先级规则配置。
- 不做有效期字段与过期出队语义。
- 不做独立异常记录页、人工重触发入口,以及资产/卡详情的优先轮询状态展示。
- 不新增可维护的参数配置项;不改普通轮询内容、并发上限、失败重试与外部调用保护。
- 不做普通轮询异常补偿场景(现状无判据,登记为后续独立 Change
- 不改人工触发 API 的任务类型白名单、每日次数上限与 24 小时去重规则。
## Capabilities
### New Capabilities
- `priority-polling-queue`: 卡轮询加急队列:触发场景与入队事实、同卡同任务类型唯一活动项与合并、优先调度与执行提示通道、认领与租约、尝试上限与出队、事实与查询的可追溯。
### Modified Capabilities
- `polling-operations`: 新增「优先轮询项与普通轮询的单次执行互斥」行为要求;既有人工触发的任务类型白名单、每日次数上限与 24 小时去重保持不变。
## Impact
- 路由与文档装配:新增后台路由与 Handler并同步文档工厂、API 组装与文档生成入口三处装配。
- Worker新增可靠事件消费者注册调度器新增优先提示排空。
- 轮询执行:纳入执行集合的轮询任务类型增加认领接缝(无活动项时行为与当前完全一致)。
- 触发写入:订单与套餐生效事务新增优先请求事件追加(同事务写入,提交后投递)。
- Schema新增成对迁移与优先项表含活动项部分唯一索引
- 常量与审计:新增触发类型、状态与尝试上限常量,以及轮询优先队列的审计动作与资源常量。

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: 优先轮询项与普通轮询的单次执行互斥
系统 SHALL 在同一卡同一轮询任务类型存在优先轮询项时只允许一个执行轮次对该卡该任务类型发起上游调用待执行优先项由本轮执行认领并作为优先执行完成该轮轮询MUST NOT 另外产生第二次调用执行中且认领租约未到期的优先项使本轮执行跳过该轮次并延后MUST NOT 发起上游调用。不存在活动优先项时,普通轮询的入队、出队、移除与延后行为 MUST 与当前行为一致。优先项存在 MUST NOT 使该卡被移出普通轮询MUST NOT 改变其它轮询任务类型的执行。
#### Scenario: 待执行优先项遇到本轮执行
- **WHEN** 该卡该轮询任务类型存在待执行优先项,且该任务类型的执行轮次到达
- **THEN** 系统认领该项并完成本轮轮询,不产生额外的第二次上游调用
#### Scenario: 已领取优先项遇到本轮的另一次执行
- **WHEN** 该卡该轮询任务类型存在执行中且认领租约未到期的优先项,该任务类型又到达一次执行
- **THEN** 系统跳过该轮次并按既有间隔延后,不发起上游调用
#### Scenario: 不存在活动优先项
- **WHEN** 该卡该轮询任务类型不存在活动优先项
- **THEN** 轮询执行的排队、入队、出队与移除行为与当前行为一致
#### Scenario: 优先项结束后的普通轮询
- **WHEN** 该卡该轮询任务类型的优先项已完成或失败出队
- **THEN** 该卡继续按普通轮询间隔执行,仍存在于普通轮询中

View File

@@ -0,0 +1,186 @@
## Purpose
在不改变既有普通轮询内容、并发上限、失败重试与外部调用保护的前提下,为购买生效、续购顺延生效、加油包生效、无有效套餐与人工加急的资产提供额外优先执行的轮询调度,并保留可追踪的加急事实。
## ADDED Requirements
### Requirement: 优先轮询触发场景与入队事实
系统 SHALL 在下列业务事实提交成功后,为关联资产的当前在用卡创建优先轮询项:主套餐购买后立即生效、排队主套餐顺延生效、加油包生效、资产无有效套餐且存在待生效的套餐使用记录。具备既有轮询手动触发权限的后台账号亦可为其数据范围内资产入队。入队 MUST 等待订单、支付与套餐使用记录提交成功;支付失败、已取消或未支付到账的订单 MUST NOT 入队。触发类型 MUST 记录为下列稳定枚举之一:`purchase_activated``renewal_activated``queue_activated``addon_activated``no_valid_package``manual_trigger`。同一载体在本次生效前已存在更早的套餐使用记录时 MUST 记为 `renewal_activated`,否则记 `purchase_activated`
#### Scenario: 主套餐购买后立即生效
- **WHEN** 一次主套餐订单支付成功,该载体当时没有在用主套餐,且套餐使用记录以生效状态提交
- **THEN** 系统为该资产的当前在用卡创建优先轮询项,触发类型为 `purchase_activated`
#### Scenario: 过期后再次购买主套餐
- **WHEN** 该载体在本次生效前已存在更早的套餐使用记录,且新的主套餐使用记录以生效状态提交
- **THEN** 触发类型记为 `renewal_activated`
#### Scenario: 排队主套餐顺延生效
- **WHEN** 一张待生效主套餐使用记录由待生效条件更新为生效,且该条件更新命中一行
- **THEN** 系统为该资产创建优先轮询项,触发类型为 `queue_activated`
#### Scenario: 加油包生效
- **WHEN** 一次加油包购买使加油包使用记录以生效状态提交
- **THEN** 系统为该资产创建优先轮询项,触发类型为 `addon_activated`
#### Scenario: 资产无有效套餐且存在待生效套餐
- **WHEN** 普通套餐轮询判定资产无有效套餐,且该载体存在待生效的套餐使用记录
- **THEN** 系统为该资产的当前在用卡创建优先轮询项,触发类型为 `no_valid_package`
#### Scenario: 无有效套餐但不存在待同步业务
- **WHEN** 普通套餐轮询判定资产无有效套餐,且该载体不存在待生效的套餐使用记录
- **THEN** 系统不创建优先轮询项,普通轮询按既有间隔继续
#### Scenario: 支付失败或订单取消
- **WHEN** 套餐订单支付失败、已取消或仍未支付到账
- **THEN** 系统不创建优先轮询项
#### Scenario: 重复支付成功回调
- **WHEN** 同一笔已生效套餐订单的支付成功回调重复投递
- **THEN** 系统不新增优先轮询项,也不产生第二次上游轮询调用
### Requirement: 入队执行对象为资产当前在用卡
优先轮询项的执行对象 MUST 是卡。触发时系统 MUST 在业务事务内冻结资产的当前在用卡快照:独立卡为卡自身,绑定设备的资产为绑定状态有效的全部在用卡。入队 MUST NOT 以设备上报的当前卡槽作为执行对象口径。同一资产存在多张在用卡时 MUST 逐卡建项,并分别记录每张卡的执行结果。
#### Scenario: 绑定设备存在多张在用卡
- **WHEN** 一个绑定设备的资产存在三张绑定状态有效的在用卡,且该资产命中触发场景
- **THEN** 系统为三张卡分别创建优先轮询项,并分别记录每张卡的执行结果
#### Scenario: 快照冻结后绑定关系变化
- **WHEN** 触发事务提交后该资产的绑定关系或设备上报的当前卡槽发生变化
- **THEN** 已冻结的执行对象不变,不追溯新增绑定卡
### Requirement: 同卡同任务类型的唯一活动项与合并
同一卡同一轮询任务类型 MUST 至多存在一条活动优先轮询项;一次优先需求对应多个既有轮询任务类型的执行。同一资产或同一卡的后续触发 MUST 合并进已存在的活动项追加触发次数、最近触发时间与来源事实MUST NOT 新建活动项、MUST NOT 重复调用同一轮上游轮询。合并 MUST NOT 因某一任务类型存在活动项而影响该卡其它任务类型的普通轮询。
#### Scenario: 自动触发与人工触发先后到达
- **WHEN** 同一卡同一任务类型已有活动优先轮询项,管理员再次手动入队
- **THEN** 系统仅追加触发来源、触发次数与最近触发时间,不创建第二条活动项,也不重复调用轮询
#### Scenario: 触发事件重放
- **WHEN** 同一触发事件被重复投递
- **THEN** 系统只合并到已有的活动项,不新增活动项
#### Scenario: 同卡不同任务类型
- **WHEN** 同一卡的某一个任务类型存在活动优先轮询项
- **THEN** 该卡其它任务类型的普通轮询执行不受该活动项影响
### Requirement: 优先调度与执行提示通道
优先轮询项 MUST 以持久化事实为权威;执行提示 MUST 通过每任务类型独立的提示通道下发MUST NOT 与既有手动触发队列共用通道键。调度器 MUST 在同一调度周期内先排空优先提示通道、再排空手动触发队列。提示通道 MUST NOT 作为活动、权限或尝试次数的判定依据。优先的强度边界 MUST 为仅调度入口领先:跳过定时队列等待并在同一周期内先入队;同一任务类型的既有执行队列 MUST NOT 被插队;分片队列背压跳过 MUST NOT 抑制优先提示入队。本能力 MUST NOT 新建调度设施、异步任务类型或队列。
#### Scenario: 提示通道丢失
- **WHEN** 执行提示通道内容丢失或缓存服务重启
- **THEN** 后续普通轮询执行仍按活动优先轮询项认领并执行,仅退化为延迟一个普通轮询周期,不丢事实也不产生第二套补偿
#### Scenario: 同一调度周期的排空顺序
- **WHEN** 优先提示通道与手动触发队列在同一调度周期都有待处理卡
- **THEN** 系统先排空优先提示通道,再排空手动触发队列
#### Scenario: 分片队列积压
- **WHEN** 某任务类型的分片队列深度超过背压阈值
- **THEN** 普通分片批次被跳过,优先提示仍按既有排空速率入队
### Requirement: 优先项认领、租约与执行前校验
领取优先轮询项 MUST 以活动项的条件更新为权威认定:仅当该项由待执行更新为执行中且该更新命中时才视为领取成功。执行中且认领租约未到期的项 MUST 拒绝其他执行重复领取。认领租约 MUST 长于既有轮询任务超时并为 90 秒;超过租约仍未被更新的执行中项 MUST 允许后续执行接管领取。执行前系统 MUST 校验该卡仍在轮询范围内(卡自身与绑定设备的轮询开关均为启用);不满足时 MUST NOT 发起上游调用MUST 以失败终态结束该项并保留可安全展示的原因。
#### Scenario: 两次执行并发到达
- **WHEN** 同一卡同一任务类型的两个执行轮次同时到达认领接缝
- **THEN** 只有一个领取成功并发起上游调用,另一个跳过该轮次并延后
#### Scenario: 领取者崩溃
- **WHEN** 一条执行中优先项超过认领租约仍未被更新
- **THEN** 相邻执行接管领取并继续执行,该卡该任务类型不被永久跳过
#### Scenario: 卡已停止轮询
- **WHEN** 该卡自身的轮询开关或绑定设备的轮询开关已关闭
- **THEN** 系统不发起上游调用,该项以失败终态出队并保留可安全展示的原因
### Requirement: 优先项尝试次数与出队
优先轮询项 SHALL 使用自身固定的最大尝试次数常量3MUST NOT 新增可维护的参数配置项MUST NOT 改变普通轮询的失败重试策略。尝试次数 MUST 只在真正发起执行后累加。未达上限的可恢复失败 MUST 回到活动状态并沿用既有轮询间隔重新排期;达到上限 MUST 记录失败终态与可安全展示的失败原因后出队。卡不存在等业务校验类失败 MUST 直接进入失败终态且不重试。执行成功 MUST 记录完成终态并出队,资产继续按普通轮询运行。本能力 MUST NOT 引入有效期字段MUST NOT 建立独立异常记录表。
#### Scenario: 可恢复失败未达上限
- **WHEN** 一次优先执行因上游调用超时、失败或响应无效而可恢复失败,且尝试次数未达上限
- **THEN** 该项回到活动状态并按既有轮询间隔重新排期
#### Scenario: 达到最大尝试次数
- **WHEN** 尝试次数达到上限仍未能成功执行
- **THEN** 该项记录失败终态与可安全展示的失败原因后出队,该卡继续普通轮询
#### Scenario: 卡不存在
- **WHEN** 优先执行时该卡已不存在
- **THEN** 该项直接进入失败终态,不重试也不发起上游调用
#### Scenario: 执行成功
- **WHEN** 优先执行按既有轮询内容完成
- **THEN** 该项记录完成终态并出队,资产继续存在于普通轮询
### Requirement: 优先轮询事实与查询的可追溯
系统 SHALL 记录入队(含合并)、领取、每次失败、重试、完成与失败出队的触发类型、触发来源、操作来源、时间与结果。人工入队 MUST 填写原因,并与入队事实一同保存。系统 MUST 提供最小可读接口(列表与详情,支持筛选与分页),其权限 MUST 与人工入队一致:超级管理员与平台账号可查询全部,代理账号仅可查询其自身及下级店铺资产,企业账号 MUST 被拒绝;查询 MUST 按数据权限下推,越权与不存在 MUST 不可区分。查询 MUST NOT 提供优先级分级、有效期或人工重触发入口。
#### Scenario: 代理查询范围外资产
- **WHEN** 代理账号查询不在其数据范围内的优先轮询项
- **THEN** 响应与查询不存在的优先轮询项不可区分
#### Scenario: 企业账号入队或查询
- **WHEN** 企业账号提交优先入队或查询优先轮询项
- **THEN** 系统拒绝
#### Scenario: 查询结果字段
- **WHEN** 授权账号查询优先轮询项列表或详情
- **THEN** 响应给出触发类型、触发来源、次数、最近触发时间与执行结果,不含优先级分级、有效期或人工重触发入口
### Requirement: 人工优先入队不受人工触发防滥用配额约束
人工优先入队 MUST NOT 受既有手工轮询的每日触发次数上限与 24 小时去重约束;这两项是人工触发的防滥用配额,不是优先队列不变量。重复抑制 MUST 由活动项合并承担,且每次人工入队 MUST 独立记录审计事实。人工入队 MUST NOT 修改调度优先级MUST NOT 绕过既有并发上限。
#### Scenario: 当日人工触发次数已达上限
- **WHEN** 操作者当日手工轮询触发次数已达既有上限后发起优先入队
- **THEN** 优先入队仍然成功并记录审计
#### Scenario: 24 小时内重复人工入队
- **WHEN** 操作者在 24 小时内对同一卡同一任务类型再次人工入队
- **THEN** 系统合并到已有活动项并记录本次人工入队审计,而不是拒绝
### Requirement: 与运营商通道阈值停机的边界
持运营商通道流量阈值停机锁的卡 MUST 保持在普通轮询范围内。优先轮询 MUST NOT 使该类卡复机;优先执行 MUST 复用既有停复机判定与持锁拒绝语义MUST NOT 绕过停机锁。
#### Scenario: 持锁卡的优先执行
- **WHEN** 持通道阈值停机锁的卡处于优先轮询执行中
- **THEN** 系统不发起复机,并保留既有的持锁拒绝事实

View File

@@ -0,0 +1,68 @@
## 1. 迁移与模型存储
- [x] 1.1 新增成对迁移 `000228_add_polling_priority_queue`:优先项表(卡、任务类型、状态、触发类型、来源订单/套餐使用记录、触发次数、来源集合、尝试次数、失败原因、人工原因与操作者、店铺快照、各时间列、软删列)、活动项部分唯一索引 `(card_id, task_type)`(仅覆盖待执行/执行中且未软删)、状态与时间索引、全列注释
- [x] 1.2 down 迁移守卫:存在活动项或未终态行时拒绝回滚并给出中文原因;与 up 严格成对
- [x] 1.3 新增优先项模型与状态/触发类型取值常量,字段长度、可空与迁移一致;不新增外键与关联标签
- [x] 1.4 新增存储层:合并插入(显式插入 + 唯一冲突识别,不用 OnConflict 打部分唯一索引)、条件认领、超租约接管、终态更新、按卡批量活动项查询、列表筛选与分页
- [x] 1.5 新增常量触发类型枚举、状态枚举、最大尝试次数3、认领租约90 秒)、优先提示通道键生成函数
## 2. 触发事件与 Worker 注册
- [x] 2.1 新增优先轮询请求事件类型与载荷版本常量;载荷含资源类型、资源 ID、冻结卡快照、触发类型、来源订单、来源套餐使用记录
- [x] 2.2 新增事件 Writer只在原业务事务内追加、幂等键稳定观测结果驱动的业务评估上下文下短路不写
- [x] 2.3 新增事件消费者:逐卡建立优先项(含合并语义)并下发执行提示;重复投递只合并
- [x] 2.4 在 Worker 组装处注册该事件类型的消费者,保持事件类型唯一注册
## 3. 生效触发点接入
- [x] 3.1 主套餐购买后立即生效路径:仅在生效状态分支追加事件;新购/续购按同载体更早套餐使用记录的存在性判定触发类型
- [x] 3.2 排队主套餐顺延生效路径:条件更新命中一行后追加事件(触发类型 `queue_activated`
- [x] 3.3 加油包生效路径:以生效状态提交后追加事件(触发类型 `addon_activated`
- [x] 3.4 在 API 与 Worker 两个组装入口注入事件 Writer
- [x] 3.5 确认支付失败、已取消与未支付到账路径不追加事件
## 4. 无有效套餐触发
- [x] 4.1 在套餐轮询评估命中「无有效套餐」处增加判定:该载体存在待生效套餐使用记录时追加事件(触发类型 `no_valid_package`),否则不追加
- [x] 4.2 该场景按资产解析绑定状态有效的全部在用卡(独立卡为自身),在触发事务内冻结快照,不使用当前卡槽口径
## 5. 互斥接缝与执行前校验
- [x] 5.1 轮询共享基类新增认领接缝:活动项索引探测 → 待执行条件认领 / 执行中跳过并延后 / 无活动项时完全等价既有行为
- [x] 5.2 四个纳入集合的轮询 Handler 接入接缝(取并发信号量之后、调用上游之前),成功、失败、跳过分支统一收口
- [x] 5.3 实现认领租约与崩溃接管:执行中且超过 90 秒租约的项允许相邻执行接管
- [x] 5.4 执行前轮询范围校验(卡自身与绑定设备的轮询开关),不满足时失败终态且不发起上游调用
- [x] 5.5 尝试次数只在真正发起执行后累加;未达上限回活动态并按既有间隔重排;达上限失败终态并保留安全原因;业务校验类失败不重试
- [x] 5.6 确认未引入通用卡级锁与 Redis 活动标记,且分片队列的出队、入队与移除路径未被改动
## 6. 提示通道与调度先排空
- [x] 6.1 队列管理新增优先提示下发:每任务类型独立键,不复用手动触发队列键
- [x] 6.2 调度器在同一周期内先排空优先提示、再排空手动触发队列;提示排空不受分片背压跳过影响
- [x] 6.3 确认未新建调度设施、未新增异步任务类型或队列
## 7. 人工入队、读侧接口与路由装配
- [x] 7.1 抽取既有人工触发权限判定为同包共享函数(超管/平台放行、代理限自身与下级、平台卡不可见、企业拒绝),判定语义不变
- [x] 7.2 人工优先入队用例:解析资产标识、权限校验、原因必填、写事实并合并、下发提示、写审计;不受每日次数上限与 24 小时去重约束
- [x] 7.3 读侧列表与详情用例:筛选、分页、按数据范围下推,越权与不存在响应不可区分
- [x] 7.4 Handler 与路由注册RouteSpec 摘要、标签、入参与出参类型),仅挂在既有后台认证中间件下
- [x] 7.5 同步文档装配三处文档工厂、API 组装与文档生成入口
- [x] 7.6 事实表冗余卡所属店铺快照,供数据范围下推使用
## 8. 审计常量与注册
- [x] 8.1 新增轮询优先队列审计动作与资源常量(入队、领取、失败、重试、完成、失败出队、人工拒绝)
- [x] 8.2 按(操作者类型,来源)注册动作定义,人工侧与 Worker 侧均能通过来源校验
- [x] 8.3 接入写入点:人工侧复用既有轮询审计输入与失败短事务模式;系统侧复用既有 Worker 审计上下文
## 9. 验证
- [x] 9.1 在受控隔离环境执行迁移 up/down/up确认 down 守卫在有活动项时拒绝回滚
- [x] 9.2 验证三种生效触发、无有效套餐(含无待同步业务不入队)、支付失败与取消不入队、重复回调与事件重放幂等
- [x] 9.3 验证合并(人工 + 自动)、同卡不同任务类型互不干扰、普通轮询互斥不产生第二次上游调用
- [x] 9.4 验证认领租约接管、提示通道丢失兜底、执行前轮询范围校验、尝试上限与终态出队
- [x] 9.5 验证权限矩阵:超管/平台全量、代理限自身与下级、范围外与平台卡不可见、企业拒绝
- [x] 9.6 验证触发类型枚举:新购/续购判定、加油包、排队顺延、无有效套餐、人工
- [x] 9.7 验证通道阈值边界:持锁卡的优先执行不复机且保留既有拒绝事实
- [x] 9.8 运行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go``openspec validate add-priority-polling-queue --strict``openspec doctor --json``./scripts/context-health.sh`;自动化测试按项目决策为 N/A