21 KiB
下游投递后台批量重投任务设计与实现符合性审计
版本:V1.1
需求确认日期:2026-08-12
文档整理日期:2026-08-13;业务口径更新:2026-08-14
适用页面:运营端 → 下游投递记录
审计基线:当前工作区HEAD=67fee216162e638ba21004fcf87e7711facefb91;本功能相关文件相对 HEAD 无未提交修改
本文目的:还原 2026-08-12 已确认的设计口径,并将当前实现逐条映射到设计,不能以“已有代码”代替“符合设计”的结论。
1. 背景与目标
下游投递记录原有两种人工操作:单条重投、勾选当前页后批量重投。它们适合少量记录,但不适合事故期间处理跨页、跨应用的大批量积压。
新增“按当前筛选条件创建后台重投任务”的目标是:
- 后端按当前真实筛选条件确定范围,不受页面分页影响;
- 创建时冻结任务快照,避免执行期间不断卷入新记录;
- 后台限速执行,客户离线或 ACK 异常时不得形成重投洪峰;
- 每条任务项可追踪、可恢复、可审计,成功确认后不得重复投递;
- 运营人员能够预检、创建、暂停、继续、终止和查看完整结果。
本功能不替代单条重投和当前页勾选重投,三种入口应同时保留。
2. 已确认的第一版业务口径
以下七条是 2026-08-12 已写入正式需求文档和测试用例的确认口径:
- 保留单条重投、当前页勾选批量重投,新增“按筛选条件重投”;投递记录分页支持每页 10/25/50 条。
- 后台任务使用企业、应用、投递类型、状态、创建日期、关键词组成的筛选快照;页码和每页条数不属于任务范围。预检生成
snapshotAt,创建任务后产生的新记录不进入该任务。 - 后台任务允许
pending、failed、unconfirmed、rejected、delivered。客户端已确认的delivered可按筛选快照再次投递,但必须醒目提示可能造成客户端重复处理;处于awaiting_ack的记录不得并发重投。创建前展示真实命中数、可重投数、规则跳过数、状态分布,任务原因必填。 - 任务按企业应用分批执行,默认每个应用 10 条/秒。单条失败不阻断整批;连续失败达到 10 条,或 ACK 超时/拒绝达到安全阈值时,自动暂停。客户离线、已有链路等待 ACK 属于“等待”,不能记作“跳过”。
- “跳过”严格表示本任务没有调用 Gateway。跳过原因包括:执行前状态变化、创建任务后才被客户确认、已被其他任务处理、本任务已成功处理、不属于任务快照、应用或投递能力已停用、投递数据不完整、缺少原 Submit 映射。只有任务项冻结的原状态已是
delivered时,才允许按已确认记录重投;其他状态在执行前收到迟到成功 ACK 时必须跳过。 - 任务支持列表、详情、暂停、继续、终止。终止只影响尚未发送的任务项。任务项以
taskId + deliveryId幂等;执行前原子认领并复核当前状态;API 重启后继续执行;已获得成功 ACK 的任务项不得再次发送。 - 创建、暂停、继续、终止、自动暂停都必须写操作日志。任务必须使用真实 PostgreSQL、Gateway 和客户 ACK,不得使用 mock、静态数据或 localStorage。
3. 用户交互设计
3.1 投递记录筛选区
筛选条件应包含:
| 条件 | 说明 |
|---|---|
| 企业 | 全部企业或指定企业 |
| 企业应用 | 受企业条件联动;全部应用或指定应用 |
| 投递类型 | 全部、状态回执、上行短信 |
| 状态 | 全部或单一状态 |
| 创建日期 | 开始日期、结束日期,按北京时间自然日 |
| 关键词 | 消息 ID、客户账号、手机号、最后错误、企业名称、应用名称 |
“按筛选条件重投”使用上表条件,但明确排除页码、每页条数和当前页勾选状态。
3.2 创建任务流程
- 用户设置筛选条件,点击“按筛选条件重投”。
- 后端在同一时点生成预检快照,返回:筛选条件、
snapshotAt、筛选命中数、可重投数、规则跳过数、状态分布、涉及应用数、最早记录时间。 - 弹窗明确告知允许
pending/failed/unconfirmed/rejected/delivered、禁止awaiting_ack,并提示已确认记录再次投递可能造成客户端重复处理。 - 用户选择执行速度,填写不少于 5 个字的事故原因、工单号或处理说明。
- 用户确认后,后端必须重新按预检的筛选快照和
snapshotAt物化任务项,而不是使用前端传入的记录 ID 列表。 - 创建成功后关闭弹窗,任务出现在任务列表,状态为“排队中”。
预检结果必须严格遵守当前筛选条件。例如当前状态选择“待投递”,命中数、可重投数和状态分布都只能基于“待投递”记录,不能擅自扩展为全部状态。
3.3 任务列表
任务列表至少展示:任务号、创建时间、企业/应用范围、任务原因、中文状态、总数、成功数、失败数、跳过数、等待数、创建人。
列表应支持分页和状态筛选,保证历史任务可查询,不能只展示固定数量的最近任务。
可执行操作:
| 当前状态 | 可执行操作 |
|---|---|
| 排队中、执行中 | 查看详情、暂停、终止 |
| 已暂停 | 查看详情、继续、终止 |
| 已完成、部分完成、已终止 | 查看详情 |
3.4 任务详情
任务详情分为三部分:
- 任务信息:筛选快照、快照时间、原因、创建人、速度、安全阈值、开始/暂停/完成时间;
- 结果汇总:排队、处理中、等待连接、等待 ACK、成功、失败、跳过、未处理;
- 完整任务项:支持分页及按结果状态、消息 ID、错误/跳过原因查询。
任务项必须使用中文状态和明确原因,不能只返回内部英文枚举,也不能只展示最近 50 项而使其余项目不可查询。
4. 后端数据设计
4.1 任务主表
DownstreamRequeueTask 保存:
- 任务号、企业范围、应用范围;
- 完整筛选快照及
snapshotAt; - 原因、执行速度、失败安全阈值;
- 任务状态和各结果计数;
- 创建人、开始时间、暂停时间、完成时间、最近错误;
- 创建时间、更新时间。
4.2 任务项表
DownstreamRequeueTaskItem 保存:
taskId、deliveryId、applicationId;- 创建任务时的原状态;
- 当前任务项状态;
- 跳过原因、失败信息;
- 认领时间、完成时间、创建时间、更新时间。
数据库必须有 taskId + deliveryId 唯一约束。任务项不能级联删除原下游投递记录,历史下游投递和 ACK 证据必须保留。
5. 状态机设计
5.1 任务状态
| 状态 | 含义 | 进入条件 |
|---|---|---|
queued |
排队中 | 创建成功或人工继续 |
running |
执行中 | 扫描器开始处理 |
paused |
已暂停 | 人工暂停或触发安全阈值 |
completed |
已完成 | 所有任务项完成且没有失败 |
partial_completed |
部分完成 | 所有任务项结束但存在失败 |
terminated |
已终止 | 人工终止;未认领项不再执行 |
5.2 任务项状态
| 状态 | 是否调用过 Gateway | 说明 |
|---|---|---|
queued |
否 | 等待认领 |
processing |
尚不确定 | 已原子认领,正在复核并准备调用 |
waiting_connection |
否或尚未写出 | 客户离线,等待可用连接,不算失败、不算跳过 |
waiting_external_ack |
否 | 其他链路已写出,等待其 ACK 结论 |
waiting_ack |
是 | 本任务已写出,等待客户 ACK |
success |
是 | 客户返回可关联原消息的成功 ACK |
failed |
是或调用失败 | Gateway 调用失败、ACK 超时或 ACK 拒绝 |
skipped |
否 | 复核后确认本任务不应调用 Gateway |
unprocessed |
否 | 任务终止时尚未开始 |
“写出成功”不能直接记为 success,只有客户有效 ACK 才是成功。
6. 执行与并发控制
- 扫描器只处理
queued/running任务。 - 多实例扫描时必须先原子认领任务项;同一任务项只能有一个执行者。
- 执行前重新读取下游投递及应用状态,再判断等待、跳过或调用 Gateway。
- 限速维度是“企业应用”,不是任务总量。一个跨 3 个应用的任务配置 10 条/秒时,每个应用各自最多 10 条/秒,并且各应用互不阻塞。
- 限速必须使用时间窗口或分布式令牌,不能依赖“定时器大约每秒执行一次 + 每轮取 N 条”,否则扫描重叠或执行耗时变化会突破或降低速率。
- 客户离线时任务项进入等待连接;客户恢复后继续,不应消耗连续失败阈值。
- 本任务写出后进入
waiting_ack;成功 ACK 转success;ACK 超时、拒绝、无效 Msg_Id 转failed并计入安全阈值。 - 连续失败只能在真正成功 ACK 后清零,不能在“写出并开始等待 ACK”时提前清零。
processing必须有租约/超时恢复。API 进程中断后,超过认领租约的项目恢复为queued并重新复核,才能满足重启续跑。- 暂停或终止与执行器并发时,执行器在认领下一项和调用 Gateway 前都必须复核任务状态。
7. 跳过、等待和失败判定
7.1 跳过
跳过意味着本任务没有调用 Gateway:
| 原因 | 判定 |
|---|---|
| 执行前状态已变化 | 已不属于允许重投状态,且不是等待 ACK |
| 创建任务后才被客户确认 | 任务项冻结原状态不是 delivered,执行前收到有效成功 ACK;避免把迟到确认变成未授权重复投递 |
| 已被其他任务处理 | 其他任务已认领、等待 ACK 或成功 |
| 本任务已成功处理 | 同任务项已有成功结果,重复扫描不得再调用 |
| 不属于任务快照 | 创建时间晚于 snapshotAt 或不再满足冻结范围 |
| 应用或投递能力已停用 | 应用状态或接口能力不允许投递 |
| 投递数据不完整 | 缺少可重放 payload 或投递类型非法 |
| 缺少原 Submit 映射 | 无法形成可关联原短信的安全回执 |
7.2 等待
- 客户离线、没有可用连接:
waiting_connection; - 其他链路已经写出且仍在 ACK 窗口:
waiting_external_ack; - 本任务已经写出:
waiting_ack。
等待项不计入跳过数或失败数。等待结束后根据真实状态继续、成功或失败。
7.3 失败
- 调用 Gateway 发生非等待型错误;
- Gateway 明确拒绝或无法安全投递;
- 本任务写出后 ACK 超时;
- 客户 ACK 非零;
- 客户 ACK 无法关联原消息。
失败原因必须保留原始错误,同时归一为可统计的失败类别。
8. 安全阈值与自动暂停
默认安全策略:
- 每应用默认 10 条/秒;
- 连续失败阈值默认 10 条;
- ACK 超时和 ACK 拒绝纳入失败阈值;
- 达到阈值时,在认领下一条之前将任务原子改为
paused; - 写操作日志,记录任务号、失败类别、连续失败数、阈值和暂停时间;
- 页面展示明确的自动暂停原因;
- 人工继续后从待处理/等待项继续,不重放已成功项。
如果多个应用同时执行,连续失败计数至少应按应用隔离,避免一个客户应用故障暂停其他正常应用;第一版若选择整任务暂停,也必须在设计和页面中明确,且仍要保存触发暂停的应用。
9. API 设计
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | /admin/operations/downstream-requeue-tasks/preview |
按当前筛选条件生成预检快照 |
| POST | /admin/operations/downstream-requeue-tasks |
使用预检快照创建任务 |
| GET | /admin/operations/downstream-requeue-tasks |
分页查询任务列表,支持状态筛选 |
| GET | /admin/operations/downstream-requeue-tasks/:id |
查询任务汇总 |
| GET | /admin/operations/downstream-requeue-tasks/:id/items |
分页查询完整任务项及原因 |
| POST | /admin/operations/downstream-requeue-tasks/:id/pause |
暂停 |
| POST | /admin/operations/downstream-requeue-tasks/:id/resume |
继续 |
| POST | /admin/operations/downstream-requeue-tasks/:id/terminate |
终止 |
创建接口不得仅信任前端回传的筛选条件和时间。建议预检生成短期有效、服务端签名的 previewToken,绑定筛选条件、snapshotAt 和操作人;创建时校验令牌,防止绕过页面篡改范围。
10. 验收用例
| 编号 | 验收点 | 预期 |
|---|---|---|
| DRQ-001 | 筛选快照 | 企业、应用、类型、状态、日期、关键词均生效;分页无关;快照后新记录不进入 |
| DRQ-002 | 预检口径 | 命中、可重投、跳过和状态分布严格基于当前筛选条件 |
| DRQ-003 | 状态白名单 | 物化 pending/failed/unconfirmed/rejected/delivered;拒绝 awaiting_ack;仅冻结原状态为 delivered 的任务项可按已确认记录重投 |
| DRQ-004 | 每应用限速 | 多应用任务中每个应用独立达到配置速度,任意扫描重叠都不超速 |
| DRQ-005 | 客户离线 | 进入等待连接,不记失败或跳过,恢复连接后继续 |
| DRQ-006 | ACK 闭环 | 写出只进入等待;有效 ACK 成功;超时、拒绝、无效 Msg_Id 失败 |
| DRQ-007 | 自动暂停 | 立即失败与 ACK 失败均计入阈值;达到阈值自动暂停并审计 |
| DRQ-008 | 并发幂等 | 多实例同时扫描,同一投递最多调用一次 Gateway |
| DRQ-009 | 重启恢复 | 在 processing、等待连接、等待 ACK 三种阶段重启 API,任务均可继续且不重复成功项 |
| DRQ-010 | 人工控制 | 暂停、继续、终止与执行并发时状态正确;终止不撤回已写出项 |
| DRQ-011 | 完整可查 | 任务列表和任务项均可分页查询全部历史数据,不限最近 10/50 条 |
| DRQ-012 | 操作审计 | 创建、暂停、继续、终止、自动暂停均有操作人/触发源和完整上下文 |
11. 当前实现符合性审计
当前实现的主要文件:
api/src/send-chain/send-downstream-requeue-task.service.tsapi/src/send-chain/send-downstream-state.service.tsapi/src/operations/admin-operations.controller.tsapi/prisma/schema.prismasrc/apps/admin/AdminDownstreamDeliveriesPage.tsxapi/src/send-chain/send-downstream-requeue-task.service.spec.ts
11.1 已符合或基本符合
| 设计项 | 结论 | 当前证据 |
|---|---|---|
| 真实持久化 | 符合 | 已有任务表、任务项表和 migration,不使用前端本地状态代替任务 |
| 快照时间上限 | 基本符合 | 创建时按 snapshotAt 限制 createdAt,快照后记录不物化 |
| 后台状态白名单 | 符合 | REPLAYABLE_STATUSES 为 pending/failed/unconfirmed/rejected/delivered |
| 禁止后台任务处理等待 ACK 筛选 | 符合 | 创建接口明确拒绝 awaiting_ack;delivered 按已确认重复投递风险口径放行 |
| 原因校验 | 符合 | 少于 5 个字拒绝 |
| 任务项幂等 | 符合 | 数据库唯一约束 taskId + deliveryId |
| 执行前任务项认领 | 基本符合 | 通过 status=queued 的条件更新认领为 processing |
| 执行前复核 | 基本符合 | 重新查询投递、应用、payload 和其他任务状态 |
| 成功 ACK 不再误发送 | 基本符合 | 非 delivered 快照项执行前才收到成功 ACK 时跳过;其他任务 success 也会阻止调用;冻结原状态为 delivered 的项目属于运营明确授权的再次投递 |
| 人工控制 | 基本符合 | 已有暂停、继续、终止接口和页面按钮 |
| 核心操作日志 | 基本符合 | 创建、暂停、继续、终止、自动暂停均写日志 |
| 投递记录分页 | 符合 | 页面支持每页 10/25/50 条并回到第一页 |
11.2 明确偏差与缺口
| 优先级 | 偏差 | 当前实现 | 与设计冲突及风险 |
|---|---|---|---|
| P0 | API 重启后 processing 项无法恢复 |
只扫描 queued,没有 processing 认领租约或超时回收 |
进程在认领后中断会使任务项永久卡住,任务永久 running,不满足重启续跑 |
| P0 | ACK 失败不参与自动暂停阈值 | reconcileWaiting把 ACK 超时/拒绝改为 failed,但不增加consecutiveFailures;进入 waiting_ack 时反而立即清零 |
客户持续拒绝或 ACK 超时不会触发安全暂停,可能持续向故障客户重投 |
| P0 | 限速不是按应用,且可能被并发扫描突破 | 每个任务每轮最多取 min(10, ratePerSecond);没有按applicationId分组,也没有分布式速率令牌;定时器可能重叠执行 |
不符合“每应用10条/秒”;配置20实际单轮最多10,多扫描器/多实例又可能超过配置 |
| P0 | 客户离线被记为任务项失败 | Gateway未写出时,底层通常返回pending/failed,任务层将非awaiting_ack/delivered结果直接记为failed |
不符合“客户离线属于等待”;会错误增加失败数并可能自动暂停 |
| P1 | 预检不严格遵守当前状态筛选 | 计算matchedCount/statusCounts时强制把状态改成all |
用户选择“待投递”时,命中数和状态分布仍可能包含其他状态;预检范围表达失真 |
| P1 | 页面缺少企业筛选 | 后端类型支持tenantId,但页面只有应用筛选,currentTaskFilter不传企业 |
未完整实现已确认的“企业 + 应用”筛选范围 |
| P1 | 任务列表不可完整查询 | 页面固定请求第1页、每页10条,没有任务分页和状态筛选 | 第11个以后历史任务在页面不可达,不满足任务列表要求 |
| P1 | 任务详情不可完整查询 | 详情接口固定返回最近50个任务项,没有任务项分页接口 | 大任务无法核对全部失败、跳过和等待项,难以验收和审计 |
| P1 | 扫描器缺少任务级/应用级分布式锁 | setInterval直接调用扫描,runScan可被重叠触发,多实例也会同时处理同一任务 |
虽有任务项条件认领可降低单项重复,但无法保证整体限速和连续失败统计一致 |
| P1 | 连续失败清零时点错误 | Gateway写出进入waiting_ack即把连续失败清零 |
写出不是业务成功;应等有效 ACK 后再清零 |
| P1 | 预检与创建没有不可篡改绑定 | 创建接口信任前端回传的filter + snapshotAt,没有预检令牌或服务端预检记录 |
可绕过页面修改筛选范围;虽仍受状态白名单和10万条上限保护,但不等于复用原预检结果 |
| P1 | 自动化覆盖远低于设计风险 | 专项仅4个测试:预检计数、参数拒绝、活动任务冲突、已确认跳过 | 未覆盖限速、多应用、离线等待、ACK阈值、重启恢复、并发扫描、完整分页及所有审计动作 |
| P2 | 状态和详情表达偏内部化 | 任务列表/详情直接展示英文状态;任务详情只展示汇总和最近项 | 运营人员不易区分等待连接、等待外部ACK、任务写出等待ACK等状态 |
| P2 | 终止后的未处理数未进入常规汇总 | 终止把queued改为unprocessed,但任务汇总只保存成功/失败/跳过/等待 |
任务进度分子可能小于总数,页面没有单独解释未处理数量 |
| P2 | 外部 ACK 跳过原因存在口径混用 | waiting_external_ack最终由其他链路成功后记为“跳过:已由其他投递链路完成” |
可以接受为“本任务未调用Gateway”,但详情必须明确这是外部链路成功,不应让用户误以为业务未处理;这与冻结原状态为 delivered 的主动再次投递是两种情形 |
11.3 综合结论
当前实现完成了数据库模型、基本预检、任务创建、任务项认领、Gateway调用、ACK结果回看和人工控制的骨架,但不能判定为严格按照 2026-08-12 方案完成。
尤其以下四项属于设计中的安全核心,当前存在实质缺口:
processing任务项缺少重启恢复;- ACK 超时/拒绝未进入自动暂停阈值;
- 每应用限速没有实现,且扫描重叠可能突破限速;
- 客户离线没有作为等待状态处理。
在上述 P0 修复并通过真实 PostgreSQL、Gateway、客户 ACK 链路测试前,不应把该后台任务认定为完整满足事故批量恢复方案。
12. 建议整改顺序
- 先补
processing租约恢复、扫描分布式锁和每应用速率令牌; - 重构任务项等待/失败状态,让离线、外部 ACK、本任务 ACK 三类等待分开;
- 将 ACK 超时、拒绝、无效 ACK 纳入按应用安全阈值,并把清零时点改为有效 ACK;
- 修正预检状态口径,增加企业筛选,并用服务端令牌绑定预检与创建;
- 增加任务列表和任务项分页、中文状态及完整原因查询;
- 补齐 DRQ-001~DRQ-012 自动化,并使用真实 PostgreSQL、Redis、Gateway 与本地客户连接完成专项验收。
整改应作为独立需求进行,不在未经授权的情况下直接修改或发布现有批量重投逻辑。