feat: harden platform workflows and UI governance

This commit is contained in:
hectorzhao
2026-07-22 14:14:55 +08:00
parent ef957f7daa
commit 0f223f7f91
80 changed files with 4958 additions and 764 deletions
+138
View File
@@ -1568,6 +1568,32 @@
- 被选候选状态变为 `claimed`,其他 pending 候选变为 `rejected`,认领动作写入操作日志。
- 认领后创建真实 `CmppDownstreamDelivery(deliveryType=uplink)`,并按现有下游投递链路在线推送或离线保留重试。
### TC-GW-028 下游人工重投并发认领与中断恢复
- 优先级:P0
- 前置条件:真实 PostgreSQL、NestJS API 和 Gateway 控制面可用;准备一条 failed/pending 下游投递记录。
- 步骤:
1. 两个运营请求同时重投同一记录,并让 Gateway pending 恢复扫描同时运行。
2. 检查数据库状态、人工次数和 Gateway 控制面调用次数。
3. 另构造一条停留在 `manual_requeueing` 且超过恢复阈值的记录,运行后台扫描。
- 预期结果:
- `id + status + updatedAt` 条件更新只允许一个运营请求认领;另一个返回明确冲突。
- 认领期间状态为 `manual_requeueing`,不进入 Gateway 的 pending 拉取结果,同一轮只调用一次控制面。
- 人工次数只增加一次;进程中断的陈旧认领自动恢复为 pending,随后由 Gateway 单一路径补投。
### TC-GW-029 Gateway提交异常幂等重入队与宕机恢复
- 优先级:P0
- 前置条件:真实 PostgreSQL、Redis Stream 和 NestJS API 可用;存在 pending Gateway 提交异常记录。
- 步骤:
1. 人工重新入队,在 Redis XADD 成功后、数据库写回 requeued 前模拟进程退出。
2. 等待陈旧 requeueing 恢复扫描,再重复调用相同幂等发布。
3. Gateway 再次上报原始 streamMessageId,并在恢复期间回传 SubmitResult。
- 预期结果:
- 恢复和重复调用返回同一个 Redis Stream IDStream 只有一个 SubmitCommand。
- 数据库最终为 requeued 或被更早 SubmitResult 闭环为 resolved,人工次数只增加一次。
- 重复异常报告不得把 requeued/resolved 回退为 pending,迟到恢复不得覆盖 resolved。
### TC-SEND-021 优先队列插队发送
- 优先级:P0
@@ -2090,6 +2116,32 @@
- 定时任务详情展示计划发送时间、创建时间、创建人、号码总数、预估费用、当前状态。
- 到点执行后的状态变化在客户端和运营端一致。
### TC-SCHEDULE-007 多实例自动调度和原子认领
- 优先级:P0
- 前置条件:启动两个连接同一 PostgreSQL/Redis 的 API 实例,存在一条已到期 scheduled 任务。
- 步骤:
1. 不调用管理端手工派发接口,等待自动扫描周期。
2. 两个实例同时扫描同一任务。
3. 查询任务、冻结流水、短信记录和 BullMQ 作业。
- 预期结果:
- 到期任务自动进入 queued/sending。
- 仅一个实例原子认领成功。
- 每条短信只存在一个以消息记录 ID 为 jobId 的队列作业,余额只冻结一次。
### TC-SCHEDULE-008 调度中断、0 元任务和超时恢复
- 优先级:P0
- 前置条件:存在普通计费任务和客户单价为 0 的免费任务,调度认领超时阈值可缩短用于测试。
- 步骤:
1. 分别在冻结后、短信转 queued 后和部分作业入队后模拟进程退出或 Redis 不可用。
2. 恢复 API/Redis 并等待认领超时后再次扫描。
3. 重复执行恢复扫描。
- 预期结果:
- 陈旧的 scheduled_dispatching/scheduled_recovering 任务能够被唯一重新认领并完成入队。
- 已存在冻结流水时不重复冻结;0 元任务入队失败时保留可恢复状态而非错误终结。
- BullMQ jobId 幂等阻止重复作业,最终任务和消息状态一致。
### TC-LOG-001 登录和登出日志
- 优先级:P1
@@ -3553,3 +3605,89 @@ npm run verify:phase8
| TC-UIUX-P0-003 | 在375视口点击删除,读取确认内容后点击取消。 | 确认层显示目标用户名;取消后用户仍在列表,PostgreSQL记录保持active且没有执行删除。 |
| TC-UIUX-P0-004 | 依次设置768×1024、1366×768、1440×900并复核同一用户。 | 平板四项操作全部可见且热区≥44px;1440首屏完整;1366即使存在内部横向滚动,滚动后删除必须完整可达,且页面级无横向溢出。 |
| TC-UIUX-P0-005 | 完成五视口操作后检查浏览器控制台并执行前端/API构建与用户服务回归。 | 无新增console error/warn;前端与API build通过,用户服务测试通过,`git diff --check`通过。 |
### 17.17 2026-07-21 下游连接恢复与历史回执回填
| 用例编号 | 操作 | 预期结果 |
| --- | --- | --- |
| TC-CMPP-DOWNSTREAM-NULL-001 | 创建一条 `status=connected`、`lastHeartbeatAt=NULL` 且 `connectedAt` 早于心跳阈值的客户接入连接,再触发新连接登记。 | 陈旧记录在连接数校验前删除,新连接不被错误的 `cmppMaxConnections` 拒绝;Gateway 能继续读取 Submit。 |
| TC-CMPP-DOWNSTREAM-NULL-002 | 创建一条刚建立、`lastHeartbeatAt=NULL` 但 `connectedAt` 尚未超过阈值的连接并执行清理。 | 新连接保留,不因首次心跳尚未写入而误删。 |
| TC-RECEIPT-BACKFILL-001 | 构造两个供应商通道共用账号,历史 DELIVRD 被写到错误通道,但同一主记录、Msg_Id、号码只有一条匹配提交记录,执行迁移两次。 | 回执改绑到唯一提交通道;主记录同步为 delivered 并写入回执状态、原始码、通道消息号和到达时间;重复执行结果不变。 |
| TC-RECEIPT-BACKFILL-002 | 构造同一历史回执能匹配零条或多条提交记录的歧义样本。 | 迁移不修改回执和主记录,保留人工核查,不以账号或模糊 Msg_Id 强行归属。 |
| TC-REPORT-BACKFILL-001 | 历史回执迁移后执行 T-4 至 T-1 重算,查询对账、利润、质量及 CSV。 | 历史成功数、收入、成本、利润和平均到达时长反映修复后的主记录与提交/回执;重复重算一致。 |
| TC-DICTIONARY-DUPLICATE-001 | 对活动或逻辑删除的全局/企业黑名单重复创建相同手机号。 | 后端返回 HTTP 409、`BLACKLIST_DUPLICATE` 和 `phoneNumber` 字段提示,不返回 500,也不创建重复数据。 |
### 17.18 双门户会话隔离、深链与锁定恢复
| 用例编号 | 操作 | 预期结果 |
| --- | --- | --- |
| TC-SESSION-DEEPLINK-001 | 未登录直接打开客户端13条受保护路由,完成验证码登录后逐条刷新。 | 登录页说明将恢复目标;登录后返回原深链;13条路由刷新后 URL 和客户端身份不变,数据来自真实 API。 |
| TC-SESSION-PORTAL-001 | 在同一浏览器先后登录运营端与客户端,并分别访问受保护页面。 | 浏览器同时持有独立 admin/client Cookie 和 localStorage;两端显示各自身份,不互相覆盖或错跳门户。 |
| TC-SESSION-LOGOUT-001 | 两端同时登录时退出客户端,再刷新运营端;反向重复。 | 只删除和广播当前门户会话;另一门户会话、页面与 Redis 记录继续有效。 |
| TC-SESSION-LOCK-001 | 锁定运营端会话,同时请求客户端当前会话;刷新锁定页面并输入当前密码解锁。 | 运营端返回 locked、客户端仍 active;锁定页说明解锁后返回当前页;解锁轮换当前门户 Cookie,原 URL 与业务数据恢复。 |
| TC-SESSION-COOKIE-001 | 同时携带 admin/client Cookie 请求两端接口,并仅携带错误门户 Cookie 重试。 | 中间件只读取路径对应 Cookie;错误门户 Cookie 返回401且不会尝试认证或泄露另一门户状态。 |
## 2026-07-21 UI/UX A2安全上传与日志导出用例
- `TC-UIUX-A2-UPLOAD-001`:客户端带伪造`x-tenant-id`上传允许用途文件,数据库`FileObject.tenantId`仍等于当前会话用户企业,MinIO对象可经客户端下载接口读回。
- `TC-UIUX-A2-UPLOAD-002`:任意用途、目录穿越和跨企业文件下载分别返回4xx,且拒绝发生在对象存储写入前。
- `TC-UIUX-A2-EXPORT-001`:两端按当前筛选导出真实日志,按钮在请求期间禁用;成功显示记录数、截断提示、操作单号和下载入口,重复点击不产生并发请求。
- `TC-UIUX-A2-EXPORT-002`:客户端忽略请求体租户,只导出当前企业;CSV表头不包含详情、IP、User-Agent,危险公式前缀被转义。
- `TC-UIUX-A2-EXPORT-003`:导出失败后页面保留筛选并提供原地重试;会话跳转恢复后可读取当前门户专用瞬时恢复条件,不错跳另一门户。
## 2026-07-21 UI/UX A3审核风险治理用例
- `TC-UIUX-A3-REVIEW-001`:待审签名缺应用、企业资料或资质文件时,预检返回具体`blockedReasons``allowedActions`不含`approve`,直接提交批准同样返回4xx。
- `TC-UIUX-A3-REVIEW-002`:完整待审签名/模板打开通过确认层,展示名称、ID、企业、应用、资格结果和影响;取消不改变数据库状态。
- `TC-UIUX-A3-REVIEW-003`:确认后按钮立即进入提交中并禁止重复点击;成功返回审计操作单号,AuditRecord记录当前会话审核人、前后状态和幂等标识。
- `TC-UIUX-A3-REVIEW-004`:相同幂等键重复请求返回同一操作单号及`replayed=true`,数据库只有一次状态变更;相同键用于不同决定返回409。
- `TC-UIUX-A3-REVIEW-005`:两个审核员使用同一`expectedUpdatedAt`并发决策,仅一个`updateMany`成功,另一个返回`REVIEW_VERSION_CONFLICT`且不得覆盖赢家。
## 2026-07-21 UI/UX A4报备生成风险治理用例
- `TC-UIUX-A4-REPORT-001`:已审核资料未绑定应用、应用停用或没有启用路由时调用预检;返回`eligible=false`和具体阻断原因,列表复选框及生成按钮不可用。
- `TC-UIUX-A4-REPORT-002`:应用路由到启用通道,但通道缺当前资料类型字段或资料缺必填值;预检逐通道返回缺失项,零可生成目标不得创建`ReportMaterialBatch`、任务或文件。
- `TC-UIUX-A4-REPORT-003`:完整资料打开生成确认层;显示企业、应用、资料版本、预计通道、运营商及成功/跳过计数,取消后数据库无批次、任务和文件变化。
- `TC-UIUX-A4-REPORT-004`:相同资料版本、应用、通道和运营商已存在成功批次时再次预检;返回既有批次并跳过。资料版本或路由运营商变化后使用新的业务键重新评估。
- `TC-UIUX-A4-REPORT-005`:相同幂等键并发或重试生成同一范围,仅产生一次批次并返回同一操作单;相同键改换资料范围返回409;按钮在请求中禁止重复提交。
- `TC-UIUX-A4-REPORT-006`:在1440×900、1366×768、768×1024、390×844和375×667打开确认层;页面无横向溢出,弹窗完整位于视口且成功/跳过/失败、取消和确认操作均可见,控制台无error/warn。
## 2026-07-21 UI/UX A5删除治理用例
- `TC-UIUX-A5-DELETE-001`:活动通道组引用通道时打开删除确认层;真实预检返回引用数量、组名和优先级,`allowedActions`为空,前后端均禁止删除且通道状态不变。
- `TC-UIUX-A5-DELETE-002`:签名仍被未删除模板、引流信息或未结束报备任务引用;运营端和客户端均显示租户内依赖摘要并禁止删除,客户端不能读取其他企业对象。
- `TC-UIUX-A5-DELETE-003`:模板存在未结束发送或批量任务时阻断;无依赖模板填写原因后逻辑删除,返回操作单号,PostgreSQL状态为`deleted`且OperationLog包含原因、依赖、影响和幂等键。
- `TC-UIUX-A5-DELETE-004`:相同删除幂等键重试返回相同操作单号且不重复审计;旧版本并发提交返回409并要求重新预检;旧删除接口不能绕过治理规则。
- `TC-UIUX-A5-DELETE-005`:在1440×900、1366×768、768×1024、390×844和375×667打开依赖确认层;对象、依赖、影响和底部操作可滚动到达,无页面级横向溢出,控制台无error/warn。
## 2026-07-21 UI/UX A6人工充值治理用例
- `TC-UIUX-A6-RECHARGE-001`:两个人工充值入口分别输入金额和备注后点击取消、右上角关闭,再次打开;金额、备注、预检和幂等状态均为空,不产生订单、流水或余额变化。
- `TC-UIUX-A6-RECHARGE-002`:输入正数或负数金额进入核对;后端返回企业名称/编码/ID、当前余额、方向、变动和预计余额,页面完整展示,返回修改不入账。
- `TC-UIUX-A6-RECHARGE-003`:最终确认后只生成一个RechargeOrder、一个AccountTransaction和一个OperationLog,账户余额等于前余额加变动金额;响应显示订单号、后余额和操作单号,操作者来自当前会话。
- `TC-UIUX-A6-RECHARGE-004`:相同幂等键并发或重试同一请求返回相同订单与操作单且`replayed=true`;键用于不同企业/金额返回409。预检后账户版本变化,旧确认请求返回409且不产生部分数据。
- `TC-UIUX-A6-RECHARGE-005`:在1440×900、1366×768、768×1024、390×844和375×667打开核对层;资金摘要、返回和最终确认可滚动到达,控制台无error/warn。
## 2026-07-22 UI/UX A7公共Dialog验收用例
| 用例ID | 场景 | 验收标准 |
| --- | --- | --- |
| A7-DIALOG-001 | 打开代表Dialog | 焦点进入弹窗;dialog由可见标题命名;背景具备inert/aria-hiddenbody滚动锁定;遮罩不可聚焦 |
| A7-DIALOG-002 | 主Dialog键盘循环 | 最后一个可用控件按Tab回到第一个,首控件按Shift+Tab回到最后一个,焦点不进入背景 |
| A7-DIALOG-003 | dirty表单按Escape/取消/关闭/遮罩 | 四种入口均打开具名alertdialog,不销毁已输入草稿,父Dialog不可交互 |
| A7-DIALOG-004 | dirty确认层键盘与返回 | 确认层Tab循环;按Escape或继续编辑后确认层关闭、草稿保留、焦点返回原字段 |
| A7-DIALOG-005 | 明确放弃 | 两层弹窗关闭、草稿销毁、背景隔离和滚动锁恢复、焦点返回原触发按钮 |
| A7-DIALOG-006 | 两端五视口视觉回归 | 运营与客户端真实登录态页面在1440×900、1366×768、768×1024、390×844、375×667无裁切或横向溢出,控制台无业务error/warn |
## 2026-07-22 UI/UX A2/A3收口用例
| 用例ID | 场景 | 验收标准 |
| --- | --- | --- |
| A23-UPLOAD-001 | 客户端企业认证页选择文件 | 请求进入`/api/client/files/upload`FileObject租户来自会话;MinIO对象字节数与上传文件一致并可回读 |
| A23-UPLOAD-002 | 伪造租户、非法用途/目录、跨租户下载 | 伪造租户不生效;非法用途或目录在对象存储写入前拒绝;跨租户文件返回404 |
| A23-UPLOAD-003 | 上传成功五视口反馈 | 1440×900、1366×768、768×1024、390×844、375×667均显示完整文件名;页面无横向溢出,步骤一可见,省市选择不裁切 |
| A23-LOG-001 | 客户端真实筛选导出 | 页面显示完成数量、操作单号和下载入口;重复点击期间按钮锁定,失败时原地重试且不跳门户 |
| A23-LOG-002 | 客户端CSV字段安全 | 表头仅六列;任意OperationLog详情、来源IP、供应商或内部字段均不得进入导出内容 |
| A23-REVIEW-001 | 签名/模板点击通过 | 首先显示对象名、唯一ID、企业、应用、资格检查和影响;没有最终确认不得改变pending状态或写AuditRecord |
| A23-REVIEW-002 | 审核并发与幂等 | 使用pending+updatedAt条件更新;同键重放返回同一操作单且仅一条审计;版本变化返回冲突 |
| A23-REVIEW-003 | 审核确认层五视口 | 签名确认层截图覆盖五视口;模板覆盖桌面截图和390px DOM尺寸测量;内容与按钮可达、无横向溢出、console无业务error/warn |