Files
junhong_cmp_fiber/openspec/specs/priority-polling-queue/spec.md
break aab56a6998
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 14m13s
feat(轮询优先队列): AUG26-016 卡轮询优先队列、人工入队与读侧接口,归档并同步主 Spec 与证据矩阵
新增 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。
2026-09-17 14:29:56 +08:00

189 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# priority-polling-queue Specification
## Purpose
在不改变既有普通轮询内容、并发上限、失败重试与外部调用保护的前提下,为购买生效、续购顺延生效、加油包生效、无有效套餐与人工加急的资产提供额外优先执行的轮询调度,并保留可追踪的加急事实。
## 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** 系统不发起复机,并保留既有的持锁拒绝事实