Files
lislgosms/docs/downstream-requeue-task-design-20260812.md

307 lines
21 KiB
Markdown
Raw Permalink 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.
# 下游投递后台批量重投任务设计与实现符合性审计
> 版本:V1.1<br>
> 需求确认日期:2026-08-12<br>
> 文档整理日期:2026-08-13;业务口径更新:2026-08-14<br>
> 适用页面:运营端 → 下游投递记录<br>
> 审计基线:当前工作区 `HEAD=67fee216162e638ba21004fcf87e7711facefb91`;本功能相关文件相对 HEAD 无未提交修改<br>
> 本文目的:还原 2026-08-12 已确认的设计口径,并将当前实现逐条映射到设计,不能以“已有代码”代替“符合设计”的结论。
## 1. 背景与目标
下游投递记录原有两种人工操作:单条重投、勾选当前页后批量重投。它们适合少量记录,但不适合事故期间处理跨页、跨应用的大批量积压。
新增“按当前筛选条件创建后台重投任务”的目标是:
1. 后端按当前真实筛选条件确定范围,不受页面分页影响;
2. 创建时冻结任务快照,避免执行期间不断卷入新记录;
3. 后台限速执行,客户离线或 ACK 异常时不得形成重投洪峰;
4. 每条任务项可追踪、可恢复、可审计,成功确认后不得重复投递;
5. 运营人员能够预检、创建、暂停、继续、终止和查看完整结果。
本功能不替代单条重投和当前页勾选重投,三种入口应同时保留。
## 2. 已确认的第一版业务口径
以下七条是 2026-08-12 已写入正式需求文档和测试用例的确认口径:
1. 保留单条重投、当前页勾选批量重投,新增“按筛选条件重投”;投递记录分页支持每页 10/25/50 条。
2. 后台任务使用企业、应用、投递类型、状态、创建日期、关键词组成的筛选快照;页码和每页条数不属于任务范围。预检生成 `snapshotAt`,创建任务后产生的新记录不进入该任务。
3. 后台任务允许 `pending``failed``unconfirmed``rejected``delivered`。客户端已确认的 `delivered` 可按筛选快照再次投递,但必须醒目提示可能造成客户端重复处理;处于 `awaiting_ack` 的记录不得并发重投。创建前展示真实命中数、可重投数、规则跳过数、状态分布,任务原因必填。
4. 任务按企业应用分批执行,默认每个应用 10 条/秒。单条失败不阻断整批;连续失败达到 10 条,或 ACK 超时/拒绝达到安全阈值时,自动暂停。客户离线、已有链路等待 ACK 属于“等待”,不能记作“跳过”。
5. “跳过”严格表示本任务没有调用 Gateway。跳过原因包括:执行前状态变化、创建任务后才被客户确认、已被其他任务处理、本任务已成功处理、不属于任务快照、应用或投递能力已停用、投递数据不完整、缺少原 Submit 映射。只有任务项冻结的原状态已是 `delivered` 时,才允许按已确认记录重投;其他状态在执行前收到迟到成功 ACK 时必须跳过。
6. 任务支持列表、详情、暂停、继续、终止。终止只影响尚未发送的任务项。任务项以 `taskId + deliveryId` 幂等;执行前原子认领并复核当前状态;API 重启后继续执行;已获得成功 ACK 的任务项不得再次发送。
7. 创建、暂停、继续、终止、自动暂停都必须写操作日志。任务必须使用真实 PostgreSQL、Gateway 和客户 ACK,不得使用 mock、静态数据或 localStorage。
## 3. 用户交互设计
### 3.1 投递记录筛选区
筛选条件应包含:
| 条件 | 说明 |
| --- | --- |
| 企业 | 全部企业或指定企业 |
| 企业应用 | 受企业条件联动;全部应用或指定应用 |
| 投递类型 | 全部、状态回执、上行短信 |
| 状态 | 全部或单一状态 |
| 创建日期 | 开始日期、结束日期,按北京时间自然日 |
| 关键词 | 消息 ID、客户账号、手机号、最后错误、企业名称、应用名称 |
“按筛选条件重投”使用上表条件,但明确排除页码、每页条数和当前页勾选状态。
### 3.2 创建任务流程
1. 用户设置筛选条件,点击“按筛选条件重投”。
2. 后端在同一时点生成预检快照,返回:筛选条件、`snapshotAt`、筛选命中数、可重投数、规则跳过数、状态分布、涉及应用数、最早记录时间。
3. 弹窗明确告知允许 `pending/failed/unconfirmed/rejected/delivered`、禁止 `awaiting_ack`,并提示已确认记录再次投递可能造成客户端重复处理。
4. 用户选择执行速度,填写不少于 5 个字的事故原因、工单号或处理说明。
5. 用户确认后,后端必须重新按预检的筛选快照和 `snapshotAt` 物化任务项,而不是使用前端传入的记录 ID 列表。
6. 创建成功后关闭弹窗,任务出现在任务列表,状态为“排队中”。
预检结果必须严格遵守当前筛选条件。例如当前状态选择“待投递”,命中数、可重投数和状态分布都只能基于“待投递”记录,不能擅自扩展为全部状态。
### 3.3 任务列表
任务列表至少展示:任务号、创建时间、企业/应用范围、任务原因、中文状态、总数、成功数、失败数、跳过数、等待数、创建人。
列表应支持分页和状态筛选,保证历史任务可查询,不能只展示固定数量的最近任务。
可执行操作:
| 当前状态 | 可执行操作 |
| --- | --- |
| 排队中、执行中 | 查看详情、暂停、终止 |
| 已暂停 | 查看详情、继续、终止 |
| 已完成、部分完成、已终止 | 查看详情 |
### 3.4 任务详情
任务详情分为三部分:
1. 任务信息:筛选快照、快照时间、原因、创建人、速度、安全阈值、开始/暂停/完成时间;
2. 结果汇总:排队、处理中、等待连接、等待 ACK、成功、失败、跳过、未处理;
3. 完整任务项:支持分页及按结果状态、消息 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. 执行与并发控制
1. 扫描器只处理 `queued/running` 任务。
2. 多实例扫描时必须先原子认领任务项;同一任务项只能有一个执行者。
3. 执行前重新读取下游投递及应用状态,再判断等待、跳过或调用 Gateway。
4. 限速维度是“企业应用”,不是任务总量。一个跨 3 个应用的任务配置 10 条/秒时,每个应用各自最多 10 条/秒,并且各应用互不阻塞。
5. 限速必须使用时间窗口或分布式令牌,不能依赖“定时器大约每秒执行一次 + 每轮取 N 条”,否则扫描重叠或执行耗时变化会突破或降低速率。
6. 客户离线时任务项进入等待连接;客户恢复后继续,不应消耗连续失败阈值。
7. 本任务写出后进入 `waiting_ack`;成功 ACK 转 `success`;ACK 超时、拒绝、无效 Msg_Id 转 `failed` 并计入安全阈值。
8. 连续失败只能在真正成功 ACK 后清零,不能在“写出并开始等待 ACK”时提前清零。
9. `processing` 必须有租约/超时恢复。API 进程中断后,超过认领租约的项目恢复为 `queued` 并重新复核,才能满足重启续跑。
10. 暂停或终止与执行器并发时,执行器在认领下一项和调用 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.ts`
- `api/src/send-chain/send-downstream-state.service.ts`
- `api/src/operations/admin-operations.controller.ts`
- `api/prisma/schema.prisma`
- `src/apps/admin/AdminDownstreamDeliveriesPage.tsx`
- `api/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 方案完成。
尤其以下四项属于设计中的安全核心,当前存在实质缺口:
1. `processing` 任务项缺少重启恢复;
2. ACK 超时/拒绝未进入自动暂停阈值;
3. 每应用限速没有实现,且扫描重叠可能突破限速;
4. 客户离线没有作为等待状态处理。
在上述 P0 修复并通过真实 PostgreSQL、Gateway、客户 ACK 链路测试前,不应把该后台任务认定为完整满足事故批量恢复方案。
## 12. 建议整改顺序
1. 先补 `processing` 租约恢复、扫描分布式锁和每应用速率令牌;
2. 重构任务项等待/失败状态,让离线、外部 ACK、本任务 ACK 三类等待分开;
3. 将 ACK 超时、拒绝、无效 ACK 纳入按应用安全阈值,并把清零时点改为有效 ACK;
4. 修正预检状态口径,增加企业筛选,并用服务端令牌绑定预检与创建;
5. 增加任务列表和任务项分页、中文状态及完整原因查询;
6. 补齐 DRQ-001DRQ-012 自动化,并使用真实 PostgreSQL、Redis、Gateway 与本地客户连接完成专项验收。
整改应作为独立需求进行,不在未经授权的情况下直接修改或发布现有批量重投逻辑。