# LisgloSIPS V2 测试方案与测试用例集 ## 1. 测试目标 验证 LisgloSIPS V2 在本地 KVM A/B/T 联调环境和后续阿里云迁移环境中,能够稳定完成“客户接入 - OpenSIPS 路由 - 计费话单 - 录音搬运 - Web 运营管理 - 监控与回滚”的核心闭环。 重点目标: - 核心通话链路可用:客户 IP/SIP 注册认证、业务前缀识别、客户网关策略、落地线路组、落地网关路由、RTP 录音、CDR 生成。 - 业务数据正确:充值幂等、余额扣减、Decimal 计费、话单归属地/运营商、录音与质检关联、审计日志完整。 - 安全边界明确:登录认证、刷新令牌轮换、RBAC、敏感字段脱敏、录音文件路径安全、SIP 非法来源和超 CPS 防护。 - 运维能力可验收:健康检查、备份恢复、发布/回滚、Prometheus/Grafana、HOMER 信令排障、Worker 故障恢复。 - 前端可运营:核心页面接真实 API,权限驱动导航和按钮裁剪,加载/空状态/错误状态可用。 ## 2. 测试范围 ### 2.1 纳入范围 - Web:登录、Dashboard、客户、客户网关、业务前缀、充值、供应商、落地网关、落地线路组、号码库、当前通话、话单、质检、用户、角色权限、操作日志。 - API:`apps/api/src/modules` 下所有已实现模块。 - Worker:CDR/Billing Worker、Recording Worker、Config Publisher。 - 数据层:Prisma/MySQL schema、Redis 热路径、Redis Stream、Outbox、幂等键。 - 通信层:Server A OpenSIPS、RTPEngine、HEP;Server T SIP 模拟脚本。 - 运维层:Nginx、TLS、systemd 服务、备份恢复、发布 preflight、回滚脚本、监控告警基础设施。 ### 2.2 不纳入范围 - 多 OpenSIPS 节点高可用。 - Kubernetes、跨地域容灾、复杂账单/发票/对账。 - 自研 SIP Trace UI、完整自研监控告警页面、完整系统设置页面。 - 超长时间满载压测。当前以功能闭环、小规模并发、故障恢复和上线前冒烟为主。 ## 3. 测试环境 | 环境 | 用途 | 说明 | | --- | --- | --- | | 本地开发 | 单元、服务、API e2e、前端构建 | Windows 工作区,Node.js 22+,pnpm 10.33.0,Vitest | | Server A | SIP/RTP/录音/HEP/Exporter | OpenSIPS、RTPEngine、录音 tmpfs、Redis 热路径 | | Server B | Web/API/Worker/DB/监控 | Nginx、API、Redis、MySQL、Worker、HOMER、Prometheus/Grafana | | Server T | 客户与落地模拟 | SIP 注册、呼叫、失败场景、并发呼叫脚本 | | 阿里云迁移环境 | 上线前复验 | 生产网络、安全组、正式 TLS、数据盘、备份恢复和灰度呼叫必须重新执行 | ## 4. 测试类型与执行策略 | 类型 | 目标 | 执行方式 | 建议频率 | | --- | --- | --- | --- | | 静态检查 | 代码规范、类型、Prisma schema 合法性 | `pnpm lint`、`pnpm typecheck`、`pnpm prisma:validate` | 每次提交前 | | 单元测试 | 认证、计费、CDR、录音路径、质检抽样、Dashboard 聚合等纯逻辑 | `pnpm test` 或定向 Vitest | 每次提交前 | | API e2e | 模块接口、权限、审计、事务和错误码 | Vitest + Supertest | 每个后端变更 | | Worker 集成 | Redis Stream、幂等、pending 重试、录音搬运校验 | Vitest + Redis/MySQL mock 或真实联调 | Worker 变更 | | 前端功能 | 菜单、表格、表单、弹窗、权限按钮、错误状态 | 浏览器人工/自动冒烟 | 前端变更 | | 三机端到端 | SIP 呼叫、路由、录音、CDR、计费、质检 | T 发起呼叫,A/B 核对数据 | 发布前、迁移前 | | 性能/故障/安全 | CPS、并发、Worker/Redis/MySQL 故障、越权和重放 | 按 S29 方法执行 | 里程碑和上线前 | | 上线验收 | 备份、preflight、灰度、回滚 | 按 S30 Runbook 执行 | 每次生产发布 | ## 5. 准入与准出标准 ### 5.1 测试准入 - 需求或缺陷边界明确,已标注影响模块。 - 数据库迁移、OpenSIPS 配置、Nginx 配置、systemd 变更均有回滚点。 - `.env.example` 或部署环境变量已补齐新增配置项。 - 本地依赖可安装,`pnpm install` 或当前锁文件状态可复现。 ### 5.2 发布准出 - `pnpm lint`、`pnpm typecheck`、`pnpm test`、`pnpm build` 通过。 - API `/api/v2/health/live` 和 `/api/v2/health/ready` 正常。 - 新 release 必须由 `pnpm release:artifact -- --release-id ` 或等价 Linux 环境命令生成,并校验 `RELEASE_MANIFEST.json` 与 `.sha256`。 - B release preflight 通过,Nginx 配置测试通过,关键 systemd 服务 active。 - 至少完成一通灰度呼叫,并核对 SIP 200 OK、CDR 入库、计费、录音 READY。 - 备份目录、SHA256 校验和回滚脚本已确认。 - 无 P0/P1 缺陷;P2 缺陷有明确规避方案和负责人。 ## 6. 自动化测试命令 在仓库根目录执行: ```powershell corepack pnpm@10.33.0 lint corepack pnpm@10.33.0 typecheck corepack pnpm@10.33.0 test corepack pnpm@10.33.0 build corepack pnpm@10.33.0 prisma:validate corepack pnpm@10.33.0 release:artifact -- --release-id sXX-name-YYYYMMDDHHmmss ``` 常用定向测试: ```powershell corepack pnpm@10.33.0 exec vitest run apps/api/src/modules/auth/auth.service.spec.ts apps/api/src/modules/auth/auth.e2e.spec.ts --hookTimeout=60000 --maxWorkers=1 corepack pnpm@10.33.0 exec vitest run packages/redis/src/cdr-stream.spec.ts --hookTimeout=60000 --maxWorkers=1 corepack pnpm@10.33.0 exec vitest run apps/worker-cdr/src/rating.spec.ts --hookTimeout=60000 --maxWorkers=1 corepack pnpm@10.33.0 exec vitest run apps/worker-recording/src/transfer.spec.ts --hookTimeout=60000 --maxWorkers=1 ``` ## 7. 测试数据准备 | 数据 | 准备方式 | 验证点 | | --- | --- | --- | | 管理员用户 | Prisma seed 或既有 B 环境内置用户 | 可登录,可访问全部核心菜单 | | 只读/质检/运营角色 | 角色权限页面或 seed | 菜单和按钮按权限裁剪 | | 客户 | 客户管理创建 | 余额、授信、状态、删除保护 | | 客户网关 | IP、SIP_DIGEST、MIXED 各一条 | 鉴权、匹配、密码不回显 | | 业务前缀 | 至少两条不同优先级前缀 | 被叫前缀识别与剥离 | | 供应商与落地网关 | 至少一条启用落地网关 | CPS、并发、禁呼时段、号码转换 | | 落地线路组 | 绑定落地网关 | 优先级、启停、并发汇总 | | 号码库 | 城市、区号、手机号段、运营商规则 | CDR 归属地/运营商识别 | | 录音样本 | T 发起真实呼叫生成 | 播放、Range、质检评分 | ## 8. 测试用例集 ### 8.1 认证、会话与权限 本节覆盖登录验证码、用户名密码校验、refresh cookie、access token、退出登录、后端 RBAC 和前端权限裁剪。后端接口统一以 `/api/v2` 为前缀。 公共前置数据: - 管理员用户 `admin_auth_001`:状态 `ENABLED`,角色包含 `admin`,权限至少包含 `dashboard.view`、`customers.view`、`customers.manage`、`users.view`、`users.manage`、`roles.view`、`quality.view`、`quality.manage`、`cdr.view`、`recordings.play`。 - 只读用户 `viewer_auth_001`:状态 `ENABLED`,仅包含 `dashboard.view`、`customers.view`。 - 质检用户 `quality_auth_001`:状态 `ENABLED`,包含 `quality.view`、`quality.manage`,不包含 `users.view`、`users.manage`。 - 禁用用户 `disabled_auth_001`:状态 `DISABLED`,密码正确但不可登录。 - 所有测试用户密码满足生产密码策略;测试结束后删除测试用户或重置为禁用状态。 #### AUTH-001 管理员登录成功 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证验证码、用户名密码、access token、refresh cookie、角色权限返回和敏感信息不泄露。 | | 前置条件 | `admin_auth_001` 启用,密码已知;API 服务正常;`AUTH_COOKIE_SECURE=false` 时本地 HTTP 测试可读取 `Set-Cookie`,生产 HTTPS 环境应为 secure cookie。 | | 测试数据 | 用户名 `admin_auth_001`;正确密码;通过 `GET /api/v2/auth/captcha` 获取 `captchaId`,自动化测试可使用注入的 `CaptchaService` 读取答案,人工测试使用图片验证码。 | | 步骤 | 1. 请求 `GET /api/v2/auth/captcha`。2. 使用正确用户名、密码、`captchaId`、`captchaCode` 请求 `POST /api/v2/auth/login`。3. 解析响应体和 `Set-Cookie`。4. 使用返回的 access token 请求一个管理员可访问接口,例如 `GET /api/v2/users`。 | | 预期结果 | 验证码接口返回 `captchaId`、`imageDataUrl`、`expiresAt`,不返回验证码答案;登录返回 200;响应体包含 `accessToken` 和 `user`;`user.roles`、`user.permissions` 非空且包含管理员权限;`Set-Cookie` 包含 refresh cookie。 | | 数据检查 | `auth_sessions` 或等价会话存储新增一条未撤销会话;库中只保存 refresh token hash,不保存明文 refresh token;用户失败次数清零。 | | 安全检查 | 响应体不包含 `passwordHash`、`sipHa1`、refresh token 明文、权限表以外的敏感字段;access token 为 Bearer token,不能通过 cookie 自动作为 access token 使用。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | AUTH-001-N1 | 缺少验证码 | 不传 `captchaId` 或 `captchaCode` 登录 | 返回 401,错误码 `AUTH_INVALID_CREDENTIALS` 或 `AUTH_CAPTCHA_INVALID`,不创建会话 | | AUTH-001-N2 | 验证码错误 | 使用错误 `captchaCode` 登录 | 返回 401 `AUTH_CAPTCHA_INVALID`,不校验出具体用户名是否存在 | | AUTH-001-N3 | 验证码复用 | 同一验证码成功登录后再次使用 | 第二次失败,验证码一次性消费 | | AUTH-001-N4 | 禁用用户登录 | 使用 `disabled_auth_001` 正确密码登录 | 返回 401 `AUTH_INVALID_CREDENTIALS`,不暴露“用户被禁用”细节 | #### AUTH-002 登录失败锁定与节流 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证连续错误密码会增加失败次数并锁定用户;大量失败会触发用户名/IP 维度节流。 | | 前置条件 | `viewer_auth_001` 启用;默认配置 `AUTH_LOCK_MAX_FAILURES=5`、`AUTH_LOCK_WINDOW_SECONDS=900`、`AUTH_LOGIN_THROTTLE_MAX_FAILURES=10`、`AUTH_LOGIN_THROTTLE_WINDOW_SECONDS=300`,如测试环境覆盖配置则按实际值执行。 | | 测试数据 | 正确用户名 `viewer_auth_001`;错误密码 `WrongPassword-Auth002`;每次登录使用新的有效验证码。 | | 步骤 | 1. 连续使用错误密码登录,次数达到锁定阈值。2. 在锁定窗口内使用正确密码登录。3. 使用不存在用户名连续失败直到节流阈值。4. 等待或模拟节流窗口过期后再次尝试。 | | 预期结果 | 阈值前每次错误返回 401 `AUTH_INVALID_CREDENTIALS`;达到锁定阈值后用户 `lockedUntil` 被设置;锁定窗口内正确密码仍返回 401;达到节流阈值后返回 429 `AUTH_LOGIN_THROTTLED`;窗口过期后节流解除。 | | 数据检查 | `failedLoginCount` 按失败次数增加;登录成功后失败次数清零;锁定只影响目标用户,不应锁定其他用户。 | | 安全检查 | 错误响应不区分“用户名不存在”“密码错误”“用户禁用”“用户锁定”;日志和审计中密码、验证码答案必须脱敏。 | 补充边界用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | AUTH-002-N1 | 大小写/空格 | 用户名前后加空格或大小写变化登录 | 按系统约定归一化;不产生绕过锁定或重复账号 | | AUTH-002-N2 | 多 IP 节流 | 同一用户名不同 IP 连续失败 | 用户名维度节流生效;IP 维度不误伤无关用户 | | AUTH-002-N3 | 锁定恢复 | 锁定窗口过期后用正确密码登录 | 登录成功,失败次数清零,`lockedUntil` 清空 | #### AUTH-003 刷新令牌轮换 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 refresh token 只能使用一次,刷新时旧会话撤销并创建新会话,防止 refresh token 重放。 | | 前置条件 | `admin_auth_001` 已成功登录,保存登录返回的 refresh cookie `cookieA`。 | | 测试数据 | `cookieA` 为登录时 `Set-Cookie`;`cookieB` 为首次 refresh 后新的 `Set-Cookie`。 | | 步骤 | 1. 携带 `cookieA` 请求 `POST /api/v2/auth/refresh`。2. 保存响应体 access token 和新的 `cookieB`。3. 再次携带旧 `cookieA` 请求 refresh。4. 携带 `cookieB` 请求 refresh。 | | 预期结果 | 第一次 refresh 返回 200,签发新 access token 和新 refresh cookie;旧 `cookieA` 再次 refresh 返回 401;`cookieB` 可以继续 refresh 并再次轮换。 | | 数据检查 | 旧 session `revokedAt` 被设置;新 session 未撤销;refresh token hash 发生变化;不会出现多个同一 refresh hash 的活跃会话。 | | 安全检查 | refresh 接口不接受 Bearer token 替代 cookie;响应体不返回 refresh token 明文;cookie 设置 `HttpOnly`、`SameSite`、合理 `Max-Age`,生产环境应包含 `Secure`。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | AUTH-003-N1 | 缺少 refresh cookie | 不带 cookie 请求 refresh | 返回 401 | | AUTH-003-N2 | 伪造 refresh cookie | 携带随机 refresh cookie | 返回 401 | | AUTH-003-N3 | 过期 refresh cookie | 使用已过期 session 对应 cookie | 返回 401,旧 session 不恢复 | #### AUTH-004 退出登录 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 logout 会撤销当前 refresh session 并清除浏览器 cookie。 | | 前置条件 | `admin_auth_001` 已登录并持有有效 refresh cookie。 | | 测试数据 | 登录后 refresh cookie。 | | 步骤 | 1. 携带 refresh cookie 请求 `POST /api/v2/auth/logout`。2. 检查响应状态和 `Set-Cookie`。3. 使用同一 cookie 请求 `POST /api/v2/auth/refresh`。4. 使用旧 access token 访问接口,记录是否仍在 access token TTL 内可用。 | | 预期结果 | logout 返回 204;响应设置 refresh cookie 过期或清空;同一 cookie refresh 返回 401;旧 access token 在过期前是否可用以系统设计为准,但不得再换取新 refresh token。 | | 数据检查 | 当前 session `revokedAt` 被设置;其他设备/其他 session 不应被误撤销,除非产品要求单点登出。 | | 安全检查 | 重复 logout 不应造成 500;无 cookie logout 返回 204 或 401 需与接口约定一致,但不得产生新会话。 | #### AUTH-005 未登录访问保护接口 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证非 Public API 必须携带有效 Bearer access token。 | | 前置条件 | API 服务正常;无需登录态。 | | 测试数据 | 受保护接口:`GET /api/v2/users`、`GET /api/v2/customers`、`GET /api/v2/recordings`;公开接口:`GET /api/v2/auth/captcha`、`POST /api/v2/auth/login`、`POST /api/v2/auth/refresh`、`GET /api/v2/health/live`。 | | 步骤 | 1. 不带 `Authorization` 请求受保护接口。2. 携带格式错误的 `Authorization: Token xxx` 请求。3. 携带伪造 JWT 请求。4. 请求公开接口。 | | 预期结果 | 受保护接口返回 401 `AUTH_REQUIRED`;格式错误和伪造 token 也返回 401;公开接口不因缺少 Bearer token 被拒绝。 | | 数据检查 | 未认证请求不写入业务数据;审计策略如记录失败访问,应不包含敏感 token 原文。 | | 安全检查 | 不应返回堆栈、JWT 校验细节、用户存在性信息。 | #### AUTH-006 权限不足访问 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 RBAC 后端强制生效,前端隐藏按钮不能作为唯一防线。 | | 前置条件 | `viewer_auth_001` 已登录,仅有 `dashboard.view`、`customers.view`;`quality_auth_001` 已登录,仅有质检权限。 | | 测试数据 | viewer access token;quality access token;管理员 access token 作为对照。 | | 步骤 | 1. 使用 viewer token 请求 `GET /api/v2/customers`。2. 使用 viewer token 请求客户写接口,例如 `POST /api/v2/customers`。3. 使用 quality token 请求 `GET /api/v2/users` 或用户写接口。4. 使用管理员 token 请求同一接口作为对照。 | | 预期结果 | viewer 可访问 `customers.view` 对应只读接口;viewer 写客户返回 403 `RBAC_FORBIDDEN`;quality 访问用户管理返回 403;管理员访问成功。 | | 数据检查 | 403 请求不产生业务变更;如写操作被拒绝,不应创建客户、充值、用户或审计成功记录。 | | 安全检查 | 403 响应只说明权限不足,不泄露所需权限全集、SQL 信息或内部角色结构。 | 补充矩阵: | 子 ID | 账号 | 接口 | 期望 | | --- | --- | --- | --- | | AUTH-006-M1 | viewer | `GET /api/v2/customers` | 200 | | AUTH-006-M2 | viewer | `POST /api/v2/customers` | 403 `RBAC_FORBIDDEN` | | AUTH-006-M3 | viewer | `GET /api/v2/users` | 403 或 200 取决于是否授予 `users.view`;本测试用户应为 403 | | AUTH-006-M4 | quality | `GET /api/v2/quality/recordings` 或质检列表接口 | 200 | | AUTH-006-M5 | quality | `GET /api/v2/users` | 403 | | AUTH-006-M6 | admin | 上述接口 | 按业务参数返回 200/201 或合法业务响应 | #### AUTH-007 前端菜单权限裁剪 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证 Web 根据登录返回的 `user.permissions` 裁剪导航,并且手动访问无权限页面时有明确反馈。 | | 前置条件 | Web 和 API 正常;`viewer_auth_001` 仅有 `dashboard.view`、`customers.view`;浏览器清空旧登录态。 | | 测试数据 | viewer 用户、admin 用户。 | | 步骤 | 1. 使用 viewer 登录 Web。2. 检查侧边栏/导航菜单。3. 手动修改 hash/path 或通过浏览器地址进入无权限页面,例如用户管理、供应商管理、质检中心。4. 退出后用 admin 登录,对照完整菜单。 | | 预期结果 | viewer 只看到 Dashboard 和客户管理等有权限页面;供应商、落地网关、质检、用户、角色、操作日志等无权限菜单不可见;手动访问无权限页面显示“无权限访问”类状态或跳转到有权限页面;admin 可看到完整授权菜单。 | | 数据检查 | viewer 登录后全局数据刷新只请求其有权限页面需要的 API,不应批量触发无权限接口 403。 | | 前端检查 | 浏览器 console 无 uncaught error;页面无空白;刷新浏览器后权限裁剪仍生效;登录返回 permissions 为空时应显示最小可用状态而不是崩溃。 | 补充菜单矩阵: | 权限 | 应显示菜单 | | --- | --- | | `dashboard.view` | 概览 Dashboard | | `customers.view` | 客户管理 | | `customer_gateways.view` | 客户网关管理、业务前缀管理 | | `recharges.view` | 充值记录 | | `vendors.view` | 供应商管理 | | `vendor_gateways.view` | 落地网关管理 | | `line_groups.view` | 落地线路组 | | `number_library.view` | 号码库 | | `active_calls.view` | 当前通话 | | `cdr.view` | 话单中心 | | `quality.view` | 质检中心 | | `users.view` | 用户管理 | | `roles.view` | 角色与权限 | | `audit.view` | 操作日志 | #### AUTH-008 按钮级权限裁剪 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证 Web 操作按钮根据 manage/play/review 类权限裁剪,同时后端仍强制校验。 | | 前置条件 | Web 和 API 正常;viewer、quality、admin 三类用户可登录;系统中至少存在一个客户、一条话单、一条 READY 录音。 | | 测试数据 | viewer 用户仅可看客户;quality 用户可看质检并评分;admin 用户全权限。 | | 步骤 | 1. viewer 登录客户管理页面,检查新增、编辑、充值/扣款、删除、启停按钮。2. viewer 进入话单/录音相关页面,检查录音播放按钮。3. quality 登录质检中心,检查抽检规则、保存评分、播放录音按钮。4. admin 登录相同页面,对照按钮完整性。5. 使用浏览器 devtools 或 API 客户端绕过前端按钮,直接调用无权限写接口。 | | 预期结果 | viewer 只能查看客户列表,不显示新增、编辑、充值/扣款、删除、启停按钮;无 `recordings.play` 时不显示或不可用播放入口;quality 按其权限显示质检评分/保存按钮;admin 显示完整操作按钮;绕过前端直接调用无权限写接口返回 403。 | | 数据检查 | 无权限用户不会产生写入、删除、充值、挂断、评分等副作用;管理员操作产生对应审计记录。 | | 前端检查 | 按钮隐藏后布局不塌陷;空状态、加载态和错误态仍可读;权限刷新后按钮状态同步更新,无需清理缓存才能生效。 | 按钮权限建议矩阵: | 页面 | 操作 | 需要权限 | 无权限表现 | | --- | --- | --- | --- | | 客户管理 | 新增/编辑/启停/删除 | `customers.manage` | 按钮不可见 | | 客户管理 | 充值/扣款 | `recharges.manage` | 按钮不可见 | | 客户网关 | 新增/编辑/启停/删除 | `customer_gateways.manage` | 按钮不可见 | | 业务前缀 | 新增/编辑/启停/删除 | `customer_gateways.manage` | 按钮不可见 | | 供应商管理 | 新增/编辑/启停/删除 | `vendors.manage` | 按钮不可见 | | 落地网关 | 新增/编辑/启停/删除 | `vendor_gateways.manage` | 按钮不可见 | | 落地线路组 | 新增/成员维护/重排/删除 | `line_groups.manage` | 按钮不可见 | | 号码库 | 导入 | `number_library.manage` | 按钮不可见 | | 当前通话 | 强制挂断 | `active_calls.manage` | 按钮不可见 | | 话单中心 | 播放录音 | `recordings.play` | 按钮不可见或禁用 | | 质检中心 | 播放录音 | `recordings.play` | 按钮不可见或禁用 | | 质检中心 | 保存评分/问题标注 | `quality.manage` | 按钮不可见 | | 用户管理 | 新增/编辑/启停/重置密码/删除 | `users.manage` | 按钮不可见 | | 角色与权限 | 新增/编辑/删除/权限维护 | `roles.manage` | 按钮不可见 | ### 8.2 客户、充值与余额 本节覆盖客户主数据、余额展示、授信/最低余额、客户状态、受控删除、客户充值/扣款流水、幂等和金额精度。后端接口统一以 `/api/v2` 为前缀。 公共前置数据: - 客户管理账号 `customer_admin_001`:包含 `customers.view`、`customers.manage`、`recharges.view`、`recharges.manage`、`audit.view`。 - 客户只读账号 `customer_viewer_001`:仅包含 `customers.view`。 - 财务账号 `finance_recharge_001`:包含 `recharges.view`、`recharges.manage`、`customers.view`,可执行人工充值和人工扣款。 - 种子客户 `cus_balance_seed_001`:状态 `ENABLED`,余额 `10.000000`,授信 `100.000000`,最低余额 `5.000000`,无删除保护关系。 - 受保护客户 `cus_protected_001`:至少关联 1 个客户网关或 1 条历史话单,用于删除保护验证。 - 所有测试金额统一按字符串传入,避免 JavaScript number 精度影响测试判断;充值/扣款接口中正数表示入金,负数表示扣款,`0` 不允许。 #### CUS-001 创建客户 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证有 `customers.manage` 权限的用户可以创建客户,默认值、金额归一化、审计和列表展示正确。 | | 前置条件 | `customer_admin_001` 已登录;客户名 `Auto Customer CUS001`、域名 `cus001.example.test` 尚不存在。 | | 测试数据 | `name=Auto Customer CUS001`,`contactName=Ops`,`phone=13800000001`,`email=ops-cus001@example.test`,`domain=cus001.example.test`,`billingMode=POSTPAID`,`creditLimit=88.5`,`minBalance=2.25`,`notes=created by CUS-001`。 | | 步骤 | 1. 使用 admin token 请求 `POST /api/v2/customers`。2. 使用返回的 `id` 请求 `GET /api/v2/customers/:id`。3. 请求 `GET /api/v2/customers` 检查列表。4. 查询操作日志或审计存储。 | | 预期结果 | 创建返回 201;客户状态为 `ENABLED`;`billingMode=POSTPAID`;`balance=0.000000` 或系统默认初始余额;`creditLimit=88.500000`;`minBalance=2.250000`;列表可见该客户。 | | 数据检查 | `customers.created_by` 为当前用户;`version` 初始化正确;`deleted_at` 为空;审计存在 `module=customers`、`action=create`、`result=SUCCESS`。 | | 安全检查 | 响应不包含数据库内部异常、无关权限信息或其他客户敏感数据。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | CUS-001-N1 | 无权限创建 | `customer_viewer_001` 请求 `POST /api/v2/customers` | 返回 403 `RBAC_FORBIDDEN`,不创建客户 | | CUS-001-N2 | 缺少客户名 | `name` 为空、空格或非字符串 | 返回 400 `VALIDATION_ERROR` | | CUS-001-N3 | 字段超长 | `name` 超 120、`email` 超 160、`notes` 超 500 | 返回 400 `VALIDATION_ERROR` | | CUS-001-N4 | 非法状态 | `status=DELETED` | 返回 400 `STATUS_INVALID` | | CUS-001-N5 | 非法计费模式 | `billingMode=MONTHLY` | 返回 400 `BILLING_MODE_INVALID` | #### CUS-002 客户唯一性校验 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证客户名称和域名唯一性,防止重复主数据影响路由和财务归属。 | | 前置条件 | 已存在客户 `Auto Customer CUS002`,域名 `cus002.example.test`。 | | 测试数据 | 同名客户请求;同域名不同名称请求;大小写或前后空格变体按产品约定执行。 | | 步骤 | 1. 创建基准客户。2. 再次使用相同 `name` 创建。3. 使用不同 `name` 但相同 `domain` 创建。4. 查询客户列表和数据库记录数。 | | 预期结果 | 重复名称返回 409 或明确业务错误;重复域名返回 409 或明确业务错误;客户记录不重复。 | | 数据检查 | 失败请求不写入 `customers`;不生成成功审计;如有失败审计,应标记 `result=FAILURE` 且包含安全脱敏后的错误摘要。 | | 安全检查 | 错误信息可以提示重复字段,但不应泄露 SQL 约束名、堆栈或数据库连接信息。 | 补充边界用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | CUS-002-N1 | 名称前后空格 | 先创建 `Acme`,再创建 ` Acme ` | 按 trim 后重复处理 | | CUS-002-N2 | 空域名重复 | 多个客户不填 `domain` | 如 schema 允许 null,应均可创建 | | CUS-002-N3 | 软删除后同名 | 软删除客户后再次创建同名客户 | 按产品规则允许或拒绝,结果需固定并记录 | #### CUS-003 客户启停与更新 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证客户状态变更、客户资料更新和余额只读边界。 | | 前置条件 | 存在客户 `cus_status_001`,状态 `ENABLED`。 | | 测试数据 | 更新 `contactName`、`phone`、`email`、`notes`;状态切换 `DISABLED`/`ENABLED`。 | | 步骤 | 1. 请求 `PATCH /api/v2/customers/:id` 更新基础资料。2. 请求 `POST /api/v2/customers/:id/disable`。3. 请求 `GET /api/v2/customers/:id`。4. 请求 `POST /api/v2/customers/:id/enable`。5. 尝试在 `PATCH` 请求体中传入 `balance` 字段。 | | 预期结果 | 资料更新成功,字段 trim/nullable 行为符合规则;禁用后状态为 `DISABLED`;启用后状态为 `ENABLED`;传入 `balance` 不应直接修改余额。 | | 数据检查 | `updated_by`、`updated_at`、`version` 按更新变化;启停分别写入 `customers:disable`、`customers:enable` 审计;余额只能通过充值或计费链路变化。 | | 安全检查 | 无 `customers.manage` 的用户更新、启停均返回 403;禁用客户不应影响其他客户。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | CUS-003-N1 | 更新不存在客户 | `PATCH /customers/not-found` | 返回 404 或明确业务错误 | | CUS-003-N2 | 无效金额字段 | `creditLimit=-1`、`minBalance=1.1234567` | 返回 400 `MONEY_INVALID` | | CUS-003-N3 | 幂等启停 | 对已禁用客户再次 disable,对已启用客户再次 enable | 不产生异常;结果状态稳定,审计按约定记录 | #### CUS-004 客户受控删除 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证客户删除为受控操作,避免删除仍被网关、话单、充值或质检引用的客户。 | | 前置条件 | `cus_protected_001` 已关联客户网关或历史话单;`cus_delete_free_001` 无关联网关、话单、充值等保护关系。 | | 测试数据 | 受保护客户 ID;可删除客户 ID。 | | 步骤 | 1. 使用 admin token 请求 `DELETE /api/v2/customers/cus_protected_001`。2. 请求 `DELETE /api/v2/customers/cus_delete_free_001`。3. 查询列表和详情。4. 查询相关关联数据是否仍完整。 | | 预期结果 | 受保护客户删除被拒绝或只允许禁用,具体按实现返回明确业务错误;无保护客户删除成功,列表默认不再展示或标记为已删除;关联数据不被级联破坏。 | | 数据检查 | 删除成功时设置 `deleted_at` 或等价软删除标记,不做物理删除;写入 `customers:delete` 审计;受保护删除失败不修改客户状态。 | | 安全检查 | 无权限删除返回 403;重复删除已删除客户返回稳定结果,不应 500。 | #### CUS-005 客户充值成功 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证客户人工充值在一个事务内完成余额增加、充值流水、幂等记录和审计。正数金额表示入金。 | | 前置条件 | `finance_recharge_001` 已登录;客户 `cus_balance_seed_001` 当前余额 `10.000000`。 | | 测试数据 | `amount=15.25`,`idempotencyKey=cus005-20260628-0001`,`remark=manual top up CUS-005`。 | | 步骤 | 1. 请求 `POST /api/v2/customers/cus_balance_seed_001/recharges`。2. 请求 `GET /api/v2/customers/cus_balance_seed_001`。3. 请求 `GET /api/v2/recharges?accountType=CUSTOMER&accountId=cus_balance_seed_001&take=10`。4. 查询审计。 | | 预期结果 | 充值返回 201;`amount=15.250000`;`beforeBalance=10.000000`;`afterBalance=25.250000`;客户余额更新为 `25.250000`;充值列表包含该流水,状态 `SUCCEEDED`。 | | 数据检查 | `customer_recharges` 或统一充值/扣款流水表新增一条;`idempotency_keys` 新增成功记录;余额和流水前后值一致;`created_by` 为财务用户。 | | 安全检查 | 只读用户请求充值返回 403;充值/扣款接口不接受前端传入 `beforeBalance`、`afterBalance` 覆盖服务端计算。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | CUS-005-N1 | 不存在客户充值/扣款 | 请求 `POST /customers/not-found/recharges` | 返回 404 或明确业务错误,不创建流水 | | CUS-005-N2 | 禁用客户充值/扣款 | 对 `DISABLED` 客户提交正数或负数金额 | 按产品规则允许或拒绝,结果需固定;若允许,客户状态不自动启用 | | CUS-005-N3 | 缺少备注 | 不传 `remark` | 充值或扣款成功,`remark=null` | | CUS-005-N4 | 备注超长 | `remark` 超 500 | 返回 400 `VALIDATION_ERROR` | #### CUS-006 客户扣款成功 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证财务可通过同一充值接口提交负数金额,对客户账户执行人工扣款,并生成可追溯流水。 | | 前置条件 | `finance_recharge_001` 已登录;客户 `cus_balance_seed_001` 当前余额 `25.250000` 或其他已知正余额;系统允许余额扣为负数或有明确最低余额规则。 | | 测试数据 | `amount=-3.25`,`idempotencyKey=cus006-deduct-0001`,`remark=manual deduction CUS-006`。 | | 步骤 | 1. 请求 `POST /api/v2/customers/cus_balance_seed_001/recharges`,提交负数 `amount`。2. 请求 `GET /api/v2/customers/cus_balance_seed_001`。3. 请求 `GET /api/v2/recharges?accountType=CUSTOMER&accountId=cus_balance_seed_001&take=10`。4. 查询审计。 | | 预期结果 | 扣款返回 201;`amount=-3.250000`;`beforeBalance` 为扣款前余额;`afterBalance=beforeBalance-3.250000`;客户余额同步减少;充值/扣款列表包含该流水,状态 `SUCCEEDED`。 | | 数据检查 | 流水应保留负数金额,不应转成正数或另建不可追踪字段;幂等 key 新增成功记录;余额和流水前后值一致;`created_by` 为财务用户。 | | 安全检查 | 只读用户请求扣款返回 403;负数金额不得绕过幂等、审计、权限或备注长度校验。 | 补充边界用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | CUS-006-N1 | 扣款后余额为 0 | 当前余额 `3.250000` 时提交 `amount=-3.25` | 成功,余额为 `0.000000` | | CUS-006-N2 | 扣款后余额为负 | 当前余额不足时提交负数扣款 | 若产品允许透支则成功并显示负余额;若不允许则返回明确业务错误,规则需固定 | | CUS-006-N3 | 负数最小精度 | 提交 `amount=-0.000001` | 成功,余额减少 `0.000001` | | CUS-006-N4 | 负零 | 提交 `amount=-0` 或 `-0.000000` | 返回 400 `MONEY_INVALID` | #### CUS-007 充值/扣款幂等重放 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证同一账号、同一请求体、同一 idempotencyKey 重放不会重复入金或重复扣款。 | | 前置条件 | 客户 `cus_balance_seed_001` 可充值/扣款;幂等 key `cus007-replay-0001` 和 `cus007-replay-deduct-0001` 未被使用。 | | 测试数据 | 入金:`amount=0.000001`,`idempotencyKey=cus007-replay-0001`;扣款:`amount=-0.000001`,`idempotencyKey=cus007-replay-deduct-0001`。 | | 步骤 | 1. 第一次提交正数充值请求。2. 立即使用完全相同请求体再次提交。3. 第一次提交负数扣款请求。4. 立即使用完全相同请求体再次提交。5. 查询客户余额、充值/扣款流水和幂等键记录。 | | 预期结果 | 每组两次请求均返回成功;每组第二次返回与第一次相同的流水 ID 和前后余额;客户余额对每个 key 只变化一次;正数和负数流水各只有一条。 | | 数据检查 | 每个 idempotencyKey 只有一个成功记录;重复请求不新增成功流水,或审计标记为幂等重放,具体按审计设计固定。 | | 安全检查 | 幂等 key 不应跨账号、跨业务类型复用成功;响应时间和错误信息不泄露其他账号充值/扣款内容。 | 补充并发用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | CUS-007-C1 | 并发充值重放 | 并发发送 5 个完全相同正数请求 | 只产生 1 条流水,最终余额只增加一次 | | CUS-007-C2 | 并发扣款重放 | 并发发送 5 个完全相同负数请求 | 只产生 1 条流水,最终余额只减少一次 | | CUS-007-C3 | 请求超时后重试 | 客户端超时后用同一 key 重试 | 返回已成功结果,不重复入金或扣款 | #### CUS-008 充值/扣款幂等冲突 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证同一 idempotencyKey 不能被不同请求体、不同账号类型或不同账号复用,尤其不能用同一 key 混用入金和扣款。 | | 前置条件 | 已使用 `idempotencyKey=cus008-conflict-0001` 对 `cus_balance_seed_001` 成功充值 `1.000000`。 | | 测试数据 | 同 key 不同 `amount`;同 key 正负号相反;同 key 不同 `remark`;同 key 不同客户;同 key 用于供应商充值。 | | 步骤 | 1. 使用相同 key、不同金额对同客户充值。2. 使用相同 key、`amount=-1.000000` 对同客户扣款。3. 使用相同 key、不同备注对同客户充值。4. 使用相同 key 对另一客户充值。5. 使用相同 key 对供应商充值。 | | 预期结果 | 所有冲突请求返回 409 `IDEMPOTENCY_KEY_CONFLICT`;不修改任何账号余额;不新增充值/扣款流水。 | | 数据检查 | 原充值记录保持不变;冲突请求可记录失败审计,但不得标记为充值或扣款成功。 | | 安全检查 | 冲突响应不返回原请求的金额、备注、客户名等敏感业务内容。 | #### CUS-009 金额精度校验 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证客户授信/最低余额和充值/扣款金额的格式、范围和归一化,避免浮点误差和非法金额进入财务数据。 | | 前置条件 | 有可创建/更新客户权限和充值权限。 | | 测试数据 | 客户金额合法值:`0`、`0.000001`、`1`、`1.2`、`99999999999999.999999`;充值/扣款合法值:`0.000001`、`-0.000001`、`1.2`、`-1.2`、`99999999999999.999999`、`-99999999999999.999999`;非法金额:客户金额字段中的负数、充值/扣款中的 `0`、`-0`、`0.000000`、`-0.000000`、`1.1234567`、`-1.1234567`、`100000000000000`、`-100000000000000`、`abc`、空字符串、科学计数法 `1e3`。 | | 步骤 | 1. 创建/更新客户时分别提交合法和非法 `creditLimit`、`minBalance`。2. 充值/扣款时分别提交合法和非法 `amount`。3. 查询返回金额格式。 | | 预期结果 | 客户金额字段允许非负,最多 14 位整数和 6 位小数,并归一化为 6 位小数;充值/扣款金额允许正数或负数,但不允许 0,绝对值最多 14 位整数和 6 位小数;非法金额返回 400 `MONEY_INVALID`。 | | 数据检查 | 非法金额请求不写入客户、充值/扣款流水或幂等成功记录;合法金额在 API、数据库、列表中均以 6 位小数字符串展示,负数保留负号。 | | 安全检查 | 不接受 NaN、Infinity、科学计数法或 JSON object/array 作为金额;服务端以 Decimal/字符串规则计算,不暴露二进制浮点误差。 | 金额矩阵: | 场景 | 字段 | 输入 | 期望 | | --- | --- | --- | --- | | 客户授信 | `creditLimit` | `0` | 成功,返回 `0.000000` | | 客户最低余额 | `minBalance` | `1.2` | 成功,返回 `1.200000` | | 充值最小金额 | `amount` | `0.000001` | 成功,余额增加 `0.000001` | | 扣款最小金额 | `amount` | `-0.000001` | 成功,余额减少 `0.000001` | | 充值/扣款零金额 | `amount` | `0`、`-0.000000` | 400 `MONEY_INVALID` | | 客户授信负数 | `creditLimit` | `-1` | 400 `MONEY_INVALID` | | 客户最低余额负数 | `minBalance` | `-1` | 400 `MONEY_INVALID` | | 小数超长 | 任意金额字段 | `1.1234567` | 400 `MONEY_INVALID` | | 整数超长 | 任意金额字段 | `100000000000000` | 400 `MONEY_INVALID` | #### CUS-010 客户列表、详情与充值/扣款流水查询 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证客户列表、客户详情和充值/扣款记录查询的只读权限、分页和筛选。 | | 前置条件 | 至少存在 2 个客户、1 条客户充值、1 条客户扣款、1 条供应商充值。 | | 测试数据 | `accountType=CUSTOMER`,`accountId=cus_balance_seed_001`,`take=10`,`skip=0`。 | | 步骤 | 1. 使用 `customer_viewer_001` 请求 `GET /api/v2/customers`。2. 请求 `GET /api/v2/customers/:id`。3. 使用 `finance_recharge_001` 或充值只读用户请求 `GET /api/v2/recharges?accountType=CUSTOMER&accountId=...`。4. 检查正数充值和负数扣款流水。5. 使用非法分页和非法 `accountType` 查询。 | | 预期结果 | 客户列表返回余额、授信、最低余额、可用余额、网关数量;客户详情字段完整;充值/扣款列表按筛选返回,包含 `items` 和 `total`;正数金额和负数金额均原样展示为 6 位小数字符串;非法 `accountType` 返回 400 `ACCOUNT_TYPE_INVALID`;非法分页返回 400 `PAGINATION_INVALID`。 | | 数据检查 | 只读查询不产生业务变更;列表默认不展示软删除客户,除非接口明确支持。 | | 安全检查 | 没有 `customers.view` 不能查客户;没有 `recharges.view` 不能查充值/扣款流水;列表不泄露其他模块敏感字段。 | ### 8.3 客户网关、业务前缀与配置发布 本节覆盖客户网关 IP/SIP/MIXED 鉴权、客户侧计费参数、主叫前缀、业务前缀、客户网关策略、配置 outbox、Config Publisher Redis 快照,以及 Web 业务前缀页面回归。后端接口统一以 `/api/v2` 为前缀。 公共前置数据: - 运营账号 `gateway_ops_001`:包含 `customer_gateways.view`、`customer_gateways.manage`、`audit.view`。 - 只读账号 `gateway_viewer_001`:仅包含 `customer_gateways.view`。 - 客户 `cus_gateway_seed_001`:状态 `ENABLED`,余额充足,未删除。 - 线路组 `llg_gateway_seed_001`:状态 `ENABLED`,至少包含 1 个可用落地网关。 - 业务前缀 `bp_671_seed`:`prefix=671`,状态 `ENABLED`,优先级 `10`。 - 客户网关 `cgw_ip_seed_001`:`authMode=IP`,`sourceIps=["100.93.185.30"]`,绑定 `llg_gateway_seed_001`。 - Redis 配置发布测试环境可读取 `cfg:active_version` 及 `cfg:v:{version}:*` key。 #### GW-001 创建 IP 认证客户网关 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 IP 鉴权客户网关创建、源 IP 校验、多 IP 去重、计费参数归一化、审计和配置发布 outbox。 | | 前置条件 | `gateway_ops_001` 已登录;客户和线路组存在;待创建网关名 `Auto IP Gateway GW001` 不重复。 | | 测试数据 | `customerId=cus_gateway_seed_001`,`name=Auto IP Gateway GW001`,`authMode=IP`,`sourceIps=["100.93.185.30","100.93.185.31","100.93.185.30"]`,`lineGroupId=llg_gateway_seed_001`,`billingCycleSec=6`,`cycleRate=0.01`,`callerMatchMode=ANY`,`calleeMatchMode=ANY`。 | | 步骤 | 1. 请求 `POST /api/v2/customer-gateways`。2. 请求 `GET /api/v2/customer-gateways?customerId=cus_gateway_seed_001`。3. 请求 `GET /api/v2/customer-gateways/:id`。4. 查询 outbox 和审计。 | | 预期结果 | 创建返回 201;`authMode=IP`;`sourceIps` 去重后保留两个 IP;`sourceIp` 兼容字段为第一个 IP;`sipUsername`、`sipDomain` 为空;`billingCycleSec=6`;`cycleRate=0.010000`;状态默认 `ENABLED`。 | | 数据检查 | 写入客户网关主表和网关 IP 明细;生成 `aggregateType=customer_gateway_config` 的待发布 outbox;审计存在 `module=customer_gateways`、`action=create`、`result=SUCCESS`。 | | 安全检查 | 无 `customer_gateways.manage` 创建返回 403;响应不包含无关客户数据或内部异常。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | GW-001-N1 | IP 鉴权缺少 IP | `authMode=IP` 但不传 `sourceIp/sourceIps` | 400 `SOURCE_IP_REQUIRED` | | GW-001-N2 | IP 格式非法 | 传 `999.1.1.1`、`abc` | 400 `SOURCE_IP_INVALID` | | GW-001-N3 | IP 数量超限 | 传超过 20 个源 IP | 400 `SOURCE_IP_TOO_MANY` | | GW-001-N4 | 非法鉴权模式 | `authMode=PASSWORD` | 400 `AUTH_MODE_INVALID` | | GW-001-N5 | 计费周期非法 | `billingCycleSec=0` 或 `86401` | 400 `INTEGER_INVALID` | | GW-001-N6 | 费率非法 | `cycleRate=-1` 或 `1.1234567` | 400 `MONEY_INVALID` | #### GW-002 创建 SIP Digest 网关 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 SIP Digest 鉴权网关创建、HA1 生成、域名归一化、密码不回显和不进入审计。 | | 前置条件 | `gateway_ops_001` 已登录;客户和线路组存在;SIP 用户名未重复。 | | 测试数据 | `authMode=SIP_DIGEST`,`sipUsername=alice-gw`,`sipDomain=SIP.EXAMPLE.LOCAL`,`sipPassword=change-me-very-strong`,`lineGroupId=llg_gateway_seed_001`,`calleeMatchMode=BUSINESS_PREFIXES`,`businessPrefixIds=["bp_671_seed"]`。 | | 步骤 | 1. 请求 `POST /api/v2/customer-gateways` 创建 SIP 网关。2. 请求列表和详情。3. 查询数据库中 `sip_ha1`。4. 查询审计日志。 | | 预期结果 | 创建返回 201;`sipDomain` 归一化为小写;`hasSipCredential=true`;列表和详情不返回 `sipPassword`、`sipHa1`;`sourceIps=[]`;业务前缀绑定可见。 | | 数据检查 | 数据库存储 HA1 摘要,值应等于 `md5(username:domain:password)`;生成配置 outbox;审计不包含明文密码。 | | 安全检查 | 明文密码不出现在 API 响应、审计 request/response、应用日志和错误信息中。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | GW-002-N1 | 缺少 SIP 用户名 | `authMode=SIP_DIGEST` 不传 `sipUsername` | 400 `VALIDATION_ERROR` | | GW-002-N2 | 缺少 SIP 域 | 不传 `sipDomain` | 400 `VALIDATION_ERROR` | | GW-002-N3 | 密码过短 | `sipPassword` 少于 12 字符 | 400 `SIP_PASSWORD_WEAK` | | GW-002-N4 | 用户名非法字符 | `sipUsername=alice space` | 400 `SIP_USERNAME_INVALID` | | GW-002-N5 | 域名非法字符 | `sipDomain=sip_example_local!` | 400 `SIP_DOMAIN_INVALID` | #### GW-003 创建 MIXED 鉴权网关 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证 MIXED 模式同时要求 IP 和 SIP 身份,支持同一网关兼容 IP 白名单与 SIP Digest。 | | 前置条件 | `gateway_ops_001` 已登录;客户和线路组存在。 | | 测试数据 | `authMode=MIXED`,`sourceIps=["100.93.185.40"]`,`sipUsername=mixed-gw`,`sipDomain=sip.example.local`,`sipPassword=change-me-very-strong`。 | | 步骤 | 1. 创建 MIXED 网关。2. 查询详情。3. 发布配置后检查 Redis IP 和 SIP 索引。 | | 预期结果 | 创建成功;响应同时包含 `sourceIps`、`sipUsername`、`sipDomain`、`hasSipCredential=true`;Redis 写入 `auth:ip:{sourceIp}` 和 `auth:sip:{username}@{domain}` 两类 key。 | | 数据检查 | 网关主表 `authMode=MIXED`;IP 明细和 SIP HA1 均存在;生成 outbox。 | | 安全检查 | 任一鉴权要素缺失均失败,不应降级为单一模式。 | #### GW-004 切换 SIP 身份要求新密码 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证修改 SIP 用户名或域名时必须重新提供 SIP 密码,避免旧 HA1 与新身份不匹配。 | | 前置条件 | 已存在 SIP_DIGEST 或 MIXED 网关 `cgw_sip_seed_001`,且 `hasSipCredential=true`。 | | 测试数据 | 新 `sipDomain=new.example.local`;新 `sipPassword=change-me-again-strong`。 | | 步骤 | 1. 不传 `sipPassword` 修改 `sipDomain`。2. 不传 `sipPassword` 修改 `sipUsername`。3. 传入新密码修改身份。4. 切换为 `authMode=IP` 并传 `sourceIps`。 | | 预期结果 | 前两步返回 400 `SIP_PASSWORD_REQUIRED`;传新密码后修改成功并重新生成 HA1;切换到 IP 后 `sipUsername/sipDomain` 清空、`hasSipCredential=false`。 | | 数据检查 | 失败请求不改变 HA1;成功修改生成配置 outbox 和 update 审计;切 IP 后不再发布 SIP auth key。 | | 安全检查 | 新旧密码均不回显、不进入审计。 | #### GW-005 主叫前缀匹配 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证客户网关可配置主叫前缀白名单,并在配置发布后进入 Redis 热路径。 | | 前置条件 | 已存在启用客户网关 `cgw_ip_seed_001`。 | | 测试数据 | `callerMatchMode=PREFIXES`,`callerPrefixes=["021","010","+8610","021"]`;另准备同 IP 其他网关已有主叫前缀 `0219` 或 `021`。 | | 步骤 | 1. 请求 `PATCH /api/v2/customer-gateways/:id` 更新主叫匹配。2. 查询网关详情。3. 执行 Config Publisher。4. 检查 Redis `cfg:v:{version}:customer_gateway:{id}:caller_prefixes`。5. 使用 T 发起匹配和不匹配主叫呼叫。 | | 预期结果 | API 返回去重后的前缀列表;发布后 Redis list 包含前缀;空业务前缀场景下按最长主叫前缀选择客户网关;匹配主叫通过客户识别,不匹配主叫返回 `CALLER_PREFIX_NOT_MATCHED` 或等价失败原因。 | | 数据检查 | `customer_gateway_caller_prefixes` 按优先级/前缀保存;生成 outbox;manifest `customerGatewayCallerPrefixCount` 增加。 | | 安全检查 | 无权限更新返回 403;非法前缀字符返回 400 `CALLER_PREFIX_INVALID`。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | GW-005-N1 | PREFIXES 缺少前缀 | `callerMatchMode=PREFIXES` 且列表为空 | 400 `CALLER_PREFIX_REQUIRED` | | GW-005-N2 | 前缀数量超限 | 超过 20 个主叫前缀 | 400 `LIST_TOO_LARGE` | | GW-005-N3 | 切回 ANY | `callerMatchMode=ANY` | 前缀列表清空,发布后不写 caller_prefixes key | | GW-005-N4 | 同网关主叫前缀覆盖 | 同一网关提交 `["021","0219"]` | 400 `CALLER_PREFIX_OVERLAP` | | GW-005-N5 | 同 IP 跨网关主叫前缀覆盖 | 同一 source IP 下另一个启用网关已有 `021`,新建/启用网关提交 `0219` | 409 `CUSTOMER_GATEWAY_CALLER_PREFIX_OVERLAP` | #### GW-006 业务前缀匹配 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证客户网关可绑定业务前缀,呼叫时先按业务前缀识别业务并剥离真实被叫。 | | 前置条件 | 业务前缀 `bp_671_seed` 和 `bp_672_seed` 均启用;客户网关启用;A 侧 OpenSIPS 加载业务前缀热路径。 | | 测试数据 | `calleeMatchMode=BUSINESS_PREFIXES`,`businessPrefixIds=["bp_671_seed","bp_672_seed"]`;测试被叫 `67113800138000`。 | | 步骤 | 1. 更新客户网关绑定业务前缀。2. 发布配置。3. 检查 Redis `business_prefix_value:671`、`customer_gateway:{id}:business_prefixes`。4. T 发起被叫带业务前缀的呼叫。5. 同时准备一个空业务前缀/主叫前缀网关作为兜底,再次发起 `671...` 呼叫。6. 查询 CDR。 | | 预期结果 | 网关详情展示绑定前缀;发布 manifest `businessPrefixCount` 和 `customerGatewayBusinessPrefixCount` 正确;呼叫按 `671` 优先匹配业务,实际被叫剥离为 `13800138000`;即使存在主叫前缀或空业务前缀兜底网关,也不得抢走已命中业务前缀的呼叫;CDR 记录业务前缀 ID/值和真实被叫。 | | 数据检查 | 只发布启用且未删除业务前缀;禁用前缀不进入全局 `business_prefixes` 列表;生成 outbox。 | | 安全检查 | 无权限绑定返回 403;跨客户绑定是否允许按产品规则固定,不得泄露其他客户敏感数据。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | GW-006-N1 | BUSINESS_PREFIXES 缺少 ID | `calleeMatchMode=BUSINESS_PREFIXES` 但不传 `businessPrefixIds` | 400 `BUSINESS_PREFIX_REQUIRED` | | GW-006-N2 | ID 数量超限 | 超过 50 个业务前缀 ID | 400 `LIST_TOO_LARGE` | | GW-006-N3 | 切回 ANY | `calleeMatchMode=ANY` | 绑定前缀清空,发布后不写 gateway business_prefixes key | | GW-006-N4 | 被叫前缀不匹配 | 发起未绑定业务前缀被叫 | 呼叫被拒绝或进入明确失败路径,CDR 失败原因可追踪 | | GW-006-N5 | 命中业务前缀但主叫不匹配 | 被叫 `671...` 命中业务前缀网关,主叫不满足该网关规则,同时存在空业务前缀兜底网关 | 返回 `CALLER_PREFIX_NOT_MATCHED`,不得退回兜底网关 | | GW-006-N6 | 同 IP 同业务前缀重复 | 同一 source IP 下新建/启用另一个绑定 `bp_671_seed` 的网关 | 409 `CUSTOMER_GATEWAY_MATCH_CONFLICT` | | GW-006-N7 | 同 IP 空业务前缀兜底重复 | 同一 source IP 下存在两个 `calleeMatchMode=ANY` 启用网关 | 409 `CUSTOMER_GATEWAY_MATCH_CONFLICT` | #### GW-006A 客户网关匹配优先级与唯一性 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证最终匹配唯一,且业务前缀优先于主叫前缀、精确规则优先于空业务前缀兜底。 | | 前置条件 | 同一测试 source IP 下准备三个候选网关:A 绑定 `bp_671_seed`,B 为 `calleeMatchMode=ANY` 且 `callerPrefixes=["021"]`,C 为 `calleeMatchMode=ANY` 且 `callerMatchMode=ANY`;三者绑定不同线路组以便观察命中结果。 | | 测试数据 | 呼叫 1:主叫 `02160010001`、被叫 `67113800138000`;呼叫 2:主叫 `02160010001`、被叫 `13800138000`;呼叫 3:主叫 `01060010001`、被叫 `13800138000`。 | | 步骤 | 1. 发布配置并确认 A 加载最新热路径。2. 分别发起三通呼叫。3. 查询 CDR、HOMER、A 日志和落地线路组。 | | 预期结果 | 呼叫 1 必须命中业务前缀网关 A,真实被叫为 `13800138000`;呼叫 2 命中主叫前缀网关 B;呼叫 3 命中空业务前缀兜底网关 C;任一场景不得出现随机网关或按 Redis 列表顺序误选。 | | 数据检查 | CDR 中客户网关、线路组、业务前缀、raw/real callee 与预期一致;客户侧计费归属客户网关,供应商成本归属落地网关。 | | 安全检查 | IP 可跨客户复用,但保存配置时必须保证上述三类呼叫最终只能命中一个网关。 | #### GW-007 业务前缀 CRUD 与优先级 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证业务前缀创建、唯一性、状态、优先级、列表筛选和受控删除。 | | 前置条件 | `gateway_ops_001` 已登录;待创建前缀 `673` 不存在。 | | 测试数据 | `prefix=673`,`name=业务673`,`description=for GW-007`,`priority=30`,`status=ENABLED`。 | | 步骤 | 1. 请求 `POST /api/v2/business-prefixes` 创建。2. 请求 `GET /api/v2/business-prefixes?status=ENABLED&keyword=673`。3. 更新 `priority` 和 `description`。4. disable/enable。5. 删除未被网关引用的前缀。 | | 预期结果 | 创建返回 201;列表按优先级和前缀展示;状态切换成功;未引用前缀可软删除;所有写操作生成配置 outbox 和审计。 | | 数据检查 | `prefix` 唯一;`priority` 在 1-9999;删除为软删除;禁用或删除后不进入 Redis 活跃业务前缀快照。 | | 安全检查 | 业务前缀接口读需要 `customer_gateways.view`,写需要 `customer_gateways.manage`;无权限返回 403。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | GW-007-N1 | 非法前缀字符 | `prefix=67#` 或中文 | 400 `BUSINESS_PREFIX_INVALID` | | GW-007-N2 | 前缀超长 | 长度超过 32 | 400 `VALIDATION_ERROR` 或 `BUSINESS_PREFIX_INVALID` | | GW-007-N3 | 优先级非法 | `priority=0`、`10000`、小数 | 400 `PRIORITY_INVALID` | | GW-007-N4 | 重复前缀 | 创建已存在 `671` | 409 或明确业务错误,不生成重复记录 | | GW-007-N5 | 删除被引用前缀 | 删除已绑定网关的前缀 | 拒绝删除或软删除后自动解除/禁用,规则需固定且不破坏网关配置 | #### GW-008 客户网关策略 CRUD 与优先级重排 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证客户网关策略按优先级匹配主叫/被叫并指向线路组,支持创建、更新、删除和重排。 | | 前置条件 | 已存在客户网关 `cgw_ip_seed_001`,线路组 `llg_gateway_seed_001`,另有备用线路组 `llg_backup_001`。 | | 测试数据 | 策略 1:`callerMode=PREFIX`、`callerValue=021`、`calleeMode=ANY`;策略 2:`callerMode=ANY`、`calleeMode=EQUALS`、`calleeValue=13800138000`;fallback 策略:`ANY/ANY`。 | | 步骤 | 1. 请求 `POST /api/v2/customer-gateways/:id/policies` 创建多条策略。2. 请求列表确认按 `priority` 排序。3. PATCH 修改策略 match mode/value。4. 请求 `POST /api/v2/customer-gateways/:id/policies/reorder` 调整顺序。5. 删除一条策略。6. 发布配置并检查 Redis policy list 顺序。 | | 预期结果 | 策略创建、更新、重排、删除成功;列表顺序与优先级一致;发布后 `customer_gateway:{id}:policies` 按优先级写入;呼叫策略选择符合重排结果。 | | 数据检查 | 每次写操作生成 `customer_gateway_config` outbox;审计包含 `policy_create`、`policy_update`、`policy_reorder`、`policy_delete`;删除为软删除或从列表移除按实现固定。 | | 安全检查 | 只读用户可列表不可写;无权限写返回 403。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | GW-008-N1 | 非 ANY 缺少匹配值 | `callerMode=PREFIX` 但不传 `callerValue` | 400 `VALIDATION_ERROR` | | GW-008-N2 | 匹配值非法字符 | `calleeValue=138 001` | 400 `MATCH_VALUE_INVALID` | | GW-008-N3 | 非法匹配模式 | `callerMode=REGEX` | 400 `MATCH_MODE_INVALID` | | GW-008-N4 | 优先级非法 | `priority=0`、`10001`、字符串 | 400 `PRIORITY_INVALID` | | GW-008-N5 | 重排列表为空 | `policyIds=[]` | 400 `POLICY_IDS_INVALID` | | GW-008-N6 | 重排遗漏/跨网关策略 | 传入不存在或其他网关策略 ID | 返回明确业务错误或仅重排本关联网关策略,规则需固定 | #### GW-009 配置发布成功 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证客户网关、业务前缀、策略变更产生 outbox 后,Config Publisher 能生成完整 Redis 配置版本并切换 active version。 | | 前置条件 | 至少存在 1 条 pending `business_prefix_config` 或 `customer_gateway_config` outbox;Redis 可用;MySQL 可读。 | | 测试数据 | 启用客户、业务前缀 `671`、IP 网关、多 IP、主叫前缀、业务前缀绑定、至少 1 条策略、线路组。 | | 步骤 | 1. 执行 Config Publisher。2. 读取 `cfg:active_version` 和 `cfg:previous_version`。3. 读取 `cfg:manifest:{version}` 或等价 manifest key。4. 检查 Redis 热路径 key。5. 检查 outbox 状态。 | | 预期结果 | Publisher 返回 `published=true`;active version 更新;manifest `schemaVersion=2` 且 counts 正确;outbox 由 `PENDING/PROCESSING` 变为 `PUBLISHED`;`lastError=null`。 | | Redis 检查 | 存在 `cfg:v:{version}:auth:ip:{sourceIp}`、`auth:ip:{sourceIp}:gateways`、`auth:sip:{username}@{domain}`、`business_prefix_value:671`、`business_prefixes`、`customer_gateway:{id}`、`customer_gateway:{id}:caller_prefixes`、`customer_gateway:{id}:business_prefixes`、`customer_gateway:{id}:policies`、`customer_gateway:{id}:line_group`。 | | 安全检查 | Redis 不应写入 SIP 明文密码;manifest checksum 稳定;发布失败前不应切换到半写入版本。 | #### GW-010 配置发布失败与重试 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证 Redis 或 MySQL 异常时 outbox 状态、错误记录和后续恢复重试行为可控。 | | 前置条件 | 存在 pending outbox;可在测试环境临时断开 Redis 或注入 Redis `multi.exec` 失败。 | | 测试数据 | 一条客户网关配置变更 outbox。 | | 步骤 | 1. 使 Redis 写入失败。2. 执行 Config Publisher。3. 检查 outbox 状态和 `lastError`。4. 恢复 Redis。5. 将失败 outbox 重置为可重试状态或按重试机制等待。6. 再次执行 Publisher。 | | 预期结果 | 失败时 outbox 标记 `FAILED` 或保留可重试状态,`attempts` 增加,`lastError` 记录截断后的错误;不切换 `cfg:active_version` 到不完整版本;恢复后可重新发布成功。 | | 数据检查 | 失败发布不丢 outbox;成功重试后 `processedAt` 设置;previous/active version 关系正确。 | | 安全检查 | `lastError` 不包含密码、连接串密码或访问令牌。 | #### GW-011 网关启停、删除与发布 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证客户网关启用、禁用、删除会影响热路径配置和呼叫准入。 | | 前置条件 | 已存在启用网关 `cgw_ip_seed_001` 并已发布。 | | 测试数据 | 网关 ID;源 IP `100.93.185.30`。 | | 步骤 | 1. 请求 `POST /api/v2/customer-gateways/:id/disable`。2. 发布配置并发起呼叫。3. 请求 enable 后再次发布并呼叫。4. 删除未被策略/话单保护的网关并发布。 | | 预期结果 | 禁用后网关状态为 `DISABLED`,呼叫被拒绝或无法匹配;启用后恢复;删除后默认列表不展示,Redis active 快照不再包含该网关可用路由。 | | 数据检查 | 启停和删除均生成 outbox 和审计;删除为软删除;有关联策略时删除行为按产品规则固定。 | | 安全检查 | 无权限启停/删除返回 403;删除不应物理清除历史 CDR 关联。 | #### GW-012 业务前缀页面加载 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 回归 Web “业务前缀管理”页面,防止默认表单常量缺失、权限裁剪或接口错误导致页面空白。 | | 前置条件 | Web/API 正常;`gateway_ops_001` 和 `gateway_viewer_001` 均可登录;后端至少存在 1 条业务前缀。 | | 测试数据 | `prefix=674`,`name=业务674`,`priority=40`。 | | 步骤 | 1. 使用 `gateway_viewer_001` 登录并进入业务前缀页面。2. 检查列表、筛选、空/错状态。3. 确认 viewer 无新增/编辑/启停/删除按钮。4. 使用 `gateway_ops_001` 登录,创建、编辑、禁用、启用一条测试前缀。5. 打开浏览器 console。 | | 预期结果 | 页面正常渲染,无空白;viewer 只读;ops 可完成 CRUD;表单默认值正确;接口错误时显示错误提示;console 无 uncaught error。 | | 数据检查 | 页面操作产生对应后端记录、outbox 和审计;删除测试前缀后列表状态正确。 | | 安全检查 | 前端隐藏按钮之外,直接调用无权限写接口仍返回 403。 | 字段校验矩阵: | 对象 | 字段 | 合法规则 | 非法示例 | 期望错误 | | --- | --- | --- | --- | --- | | 客户网关 | `authMode` | `IP`、`SIP_DIGEST`、`MIXED` | `PASSWORD` | `AUTH_MODE_INVALID` | | 客户网关 | `sourceIps` | IP/MIXED 必填,最多 20 个 IPv4/IPv6 | `abc`、超过 20 个 | `SOURCE_IP_REQUIRED`、`SOURCE_IP_INVALID`、`SOURCE_IP_TOO_MANY` | | 客户网关 | `sipUsername` | SIP/MIXED 必填,`A-Za-z0-9_.:+-` | `alice gw` | `SIP_USERNAME_INVALID` | | 客户网关 | `sipDomain` | SIP/MIXED 必填,字母数字点横线,保存为小写 | `sip_domain!` | `SIP_DOMAIN_INVALID` | | 客户网关 | `sipPassword` | SIP/MIXED 创建或身份变更时必填,至少 12 字符 | `short` | `SIP_PASSWORD_WEAK`、`SIP_PASSWORD_REQUIRED` | | 客户网关 | `billingCycleSec` | 1-86400 整数 | `0`、`86401`、`1.5` | `INTEGER_INVALID` | | 客户网关 | `cycleRate` | 非负,最多 14 位整数、6 位小数 | `-1`、`1.1234567` | `MONEY_INVALID` | | 客户网关 | `callerPrefixes` | PREFIXES 模式必填,最多 20 个,安全字符 | 空、含空格、超过 20 个 | `CALLER_PREFIX_REQUIRED`、`CALLER_PREFIX_INVALID`、`LIST_TOO_LARGE` | | 客户网关 | `businessPrefixIds` | BUSINESS_PREFIXES 模式必填,最多 50 个 | 空、超过 50 个 | `BUSINESS_PREFIX_REQUIRED`、`LIST_TOO_LARGE` | | 客户网关 | 匹配唯一性 | 同一 IP + 同一业务前缀唯一;同一 IP 主叫前缀不互相覆盖;空业务前缀兜底唯一 | 重复 `bp_671_seed`、`021` 与 `0219`、两个 ANY 被叫兜底 | `CUSTOMER_GATEWAY_MATCH_CONFLICT`、`CUSTOMER_GATEWAY_CALLER_PREFIX_OVERLAP` | | 业务前缀 | `prefix` | 1-32 位字母或数字,唯一 | `67#`、中文、重复 | `BUSINESS_PREFIX_INVALID` 或 409 | | 业务前缀 | `priority` | 1-9999 整数 | `0`、`10000`、`1.5` | `PRIORITY_INVALID` | | 策略 | `callerMode/calleeMode` | `ANY`、`EQUALS`、`PREFIX` | `REGEX` | `MATCH_MODE_INVALID` | | 策略 | `callerValue/calleeValue` | 非 ANY 时必填,最多 64 位安全字符 | 空、`138 001` | `VALIDATION_ERROR`、`MATCH_VALUE_INVALID` | ### 8.4 供应商、落地网关与线路组 本节覆盖供应商主数据、供应商充值、落地网关出局参数、禁呼时段、编码、主被叫改写、主叫号码池、线路组成员、优先级、并发汇总、配置发布和真实呼叫选路。后端接口统一以 `/api/v2` 为前缀。 公共前置数据: - 供应商管理账号 `vendor_ops_001`:包含 `vendors.view`、`vendors.manage`、`vendor_gateways.view`、`vendor_gateways.manage`、`line_groups.view`、`line_groups.manage`、`recharges.view`、`recharges.manage`、`audit.view`。 - 只读账号 `vendor_viewer_001`:包含 `vendors.view`、`vendor_gateways.view`、`line_groups.view`。 - 供应商 `ven_seed_001`:状态 `ENABLED`,余额 `20.000000`,授信 `200.000000`。 - 落地网关 `vgw_primary_001`:状态 `ENABLED`,host 指向 T/UAS 或可控落地模拟端。 - 落地网关 `vgw_backup_001`:状态 `ENABLED`,用于线路组故障转移。 - 线路组 `llg_vendor_seed_001`:状态 `ENABLED`,至少包含 `vgw_primary_001` 和 `vgw_backup_001`。 - 客户网关策略已指向 `llg_vendor_seed_001`,用于真实呼叫选路验证。 #### VEN-001 创建供应商 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证供应商主数据创建、默认余额/授信、可用余额、审计和权限。 | | 前置条件 | `vendor_ops_001` 已登录;供应商名 `Auto Vendor VEN001` 不存在。 | | 测试数据 | `name=Auto Vendor VEN001`,`contactName=NOC`,`phone=4001000001`,`email=noc-ven001@example.test`,`creditLimit=188.5`,`settlement=monthly`,`notes=created by VEN-001`。 | | 步骤 | 1. 请求 `POST /api/v2/vendors`。2. 使用返回 ID 请求 `GET /api/v2/vendors/:id`。3. 请求 `GET /api/v2/vendors`。4. 查询审计。 | | 预期结果 | 创建返回 201;状态默认 `ENABLED`;余额为系统默认值;`creditLimit=188.500000`;`availableBalance=balance+creditLimit`;列表可见,`gatewayCount=0`。 | | 数据检查 | 写入 `created_by`、`updated_at`、`deleted_at=null`;审计存在 `module=vendors`、`action=create`、`result=SUCCESS`。 | | 安全检查 | `vendor_viewer_001` 创建返回 403;响应不包含内部异常或其他供应商敏感字段。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | VEN-001-N1 | 缺少名称 | `name` 为空、空格或非字符串 | 400 `VALIDATION_ERROR` | | VEN-001-N2 | 字段超长 | `name` 超 120、`settlement` 超 80、`notes` 超 500 | 400 `VALIDATION_ERROR` | | VEN-001-N3 | 非法状态 | `status=DELETED` | 400 `STATUS_INVALID` | | VEN-001-N4 | 授信金额非法 | `creditLimit=-1` 或 `1.1234567` | 400 `MONEY_INVALID` | | VEN-001-N5 | 重复名称 | 创建已存在供应商名称 | 409 或明确业务错误,不重复写入 | #### VEN-002 供应商更新、启停与删除保护 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证供应商资料更新、启停、软删除,以及有关联落地网关时的删除保护。 | | 前置条件 | 已存在供应商 `ven_seed_001`;另有无关联供应商 `ven_delete_free_001`。 | | 测试数据 | 更新 `notes=primary carrier`,`creditLimit=200`。 | | 步骤 | 1. PATCH 更新供应商资料。2. disable 后查询状态。3. enable 后查询状态。4. 删除有关联落地网关的供应商。5. 删除无关联供应商。 | | 预期结果 | 更新成功且金额归一化;启停状态正确;有关联供应商删除被拒绝或按产品规则受控软删除;无关联供应商可软删除。 | | 数据检查 | `updated_by`、`version` 或更新时间更新;启停/删除均写审计;删除不物理清除历史充值和网关引用。 | | 安全检查 | 无 `vendors.manage` 的更新、启停、删除返回 403。 | #### VEN-003 供应商充值 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证供应商人工充值会增加供应商余额、生成充值流水、幂等记录和审计。 | | 前置条件 | `vendor_ops_001` 已登录;供应商 `ven_seed_001` 当前余额已知。 | | 测试数据 | `amount=3.5`,`idempotencyKey=ven003-20260628-0001`,`remark=vendor top up VEN-003`。 | | 步骤 | 1. 请求 `POST /api/v2/vendors/ven_seed_001/recharges`。2. 查询供应商详情。3. 请求 `GET /api/v2/recharges?accountType=VENDOR&accountId=ven_seed_001`。4. 重放相同请求。 | | 预期结果 | 第一次返回 201;`amount=3.500000`;`afterBalance=beforeBalance+3.500000`;供应商详情余额同步增加;重放返回同一流水 ID,不重复加钱。 | | 数据检查 | 充值流水 `accountType=VENDOR`;幂等 key 成功记录唯一;审计存在 `module=recharges`、`action=vendor_recharge`。 | | 安全检查 | 无 `recharges.manage` 返回 403;响应和审计不允许前端覆盖 `beforeBalance/afterBalance`。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | VEN-003-N1 | 不存在供应商充值 | `POST /vendors/not-found/recharges` | 404 或明确业务错误 | | VEN-003-N2 | 幂等冲突 | 同 key 修改金额或备注 | 409 `IDEMPOTENCY_KEY_CONFLICT` | | VEN-003-N3 | 金额非法 | `0`、负数、超 6 位小数、超长金额 | 400 `MONEY_INVALID`,不写流水 | | VEN-003-N4 | key 非法 | 少于 8 位或含空格 | 400 `IDEMPOTENCY_KEY_INVALID` | #### VGW-001 创建 IP 落地网关 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证基础 IP 落地网关创建、host/port/transport、CPS/并发、计费周期/费率和配置发布 outbox。 | | 前置条件 | `vendor_ops_001` 已登录;供应商 `ven_seed_001` 存在。 | | 测试数据 | `vendorId=ven_seed_001`,`name=Auto IP Vendor Gateway`,`authMode=IP`,`host=203.0.113.10`,`port=5060`,`transport=udp`,`cpsLimit=30`,`concurrencyLimit=300`,`billingCycleSec=6`,`cycleRate=0.012`,`landingCalleePrefix=86`。 | | 步骤 | 1. 请求 `POST /api/v2/vendor-gateways`。2. 请求列表和详情。3. 查询 outbox 和审计。 | | 预期结果 | 创建返回 201;host 小写/规范化;`minuteRate=cycleRate*60/billingCycleSec=0.120000`;状态默认 `ENABLED`;列表不返回 SIP HA1。 | | 数据检查 | 写入 `vendor_gateway_config` outbox;审计存在 `module=vendor_gateways`、`action=create`;供应商 `gatewayCount` 增加。 | | 安全检查 | `vendor_viewer_001` 创建返回 403;响应不包含 SIP 密钥或内部错误。 | #### VGW-002 创建 SIP/MIXED 落地网关 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证落地侧 SIP Digest/MIXED 鉴权、HA1 生成、密码不回显和身份变更规则。 | | 前置条件 | 供应商存在;SIP 用户名未重复。 | | 测试数据 | `authMode=SIP_DIGEST`,`host=SIP.CARRIER.LOCAL`,`sipUsername=carrier-user`,`sipPassword=change-me-very-strong`。 | | 步骤 | 1. 创建 SIP_DIGEST 落地网关。2. 查询详情和审计。3. 修改 host 或 sipUsername 但不传 `sipPassword`。4. 传新密码后修改。5. 切换为 IP。 | | 预期结果 | 创建成功,host 归一化小写,`hasSipCredential=true`;响应/审计不含明文密码和 HA1;身份变更不传密码返回 400 `SIP_PASSWORD_REQUIRED`;切 IP 后 SIP 身份清空。 | | 数据检查 | HA1 为 `md5(username:host:password)`;每次成功写操作生成 outbox。 | | 安全检查 | 密码过短返回 `SIP_PASSWORD_WEAK`,但不回显密码。 | #### VGW-003 禁呼时段校验与生效 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证落地网关可配置禁呼时段,并在呼叫选路时避开禁呼网关。 | | 前置条件 | 落地网关 `vgw_primary_001` 存在;线路组有备用网关。 | | 测试数据 | `forbiddenPeriods=[{weekdayMask:62,startTime:"23:00:00",endTime:"23:59:59"}]`。 | | 步骤 | 1. PATCH 更新禁呼时段。2. 发布配置。3. 在禁呼时段内发起呼叫或模拟时间。4. 在非禁呼时段发起呼叫。 | | 预期结果 | 禁呼时段内该网关不被选中,若有备用则转备用,否则呼叫按规则失败;非禁呼时段可选中。 | | 数据检查 | Redis `vendor_gateway:{id}` 中包含 forbiddenPeriods;发布 manifest 版本更新。 | | 安全检查 | 无权限更新返回 403;禁呼配置失败不应影响已发布版本。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | VGW-003-N1 | 非数组 | `forbiddenPeriods={}` | 400 `FORBIDDEN_PERIODS_INVALID` | | VGW-003-N2 | 星期掩码非法 | `weekdayMask=0` 或 `128` | 400 `INTEGER_INVALID` | | VGW-003-N3 | 时间格式非法 | `startTime=24:00:00` 或 `23:00` | 400 `TIME_INVALID` | | VGW-003-N4 | 跨午夜时段 | `startTime=23:00:00`,`endTime=01:00:00` | 按产品规则允许或拒绝,行为需固定 | #### VGW-004 编码与媒体能力配置 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证落地网关 codec 列表、优先级唯一性和发布快照。 | | 前置条件 | 落地网关存在。 | | 测试数据 | `codecs=[{codec:"PCMA",priority:1},{codec:"PCMU",priority:2},{codec:"G729",priority:3}]`。 | | 步骤 | 1. PATCH 更新 codecs。2. 查询详情。3. 发布配置并检查 Redis。4. 发起呼叫检查 SDP/媒体协商是否符合预期。 | | 预期结果 | API 返回 codec 大写且按优先级展示;发布快照包含 codecs;媒体协商优先使用配置内 codec。 | | 数据检查 | codec 值唯一,priority 唯一;生成 outbox。 | | 安全检查 | 非法 codec 不进入配置快照。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | VGW-004-N1 | codec 非数组 | `codecs="PCMA"` | 400 `CODECS_INVALID` | | VGW-004-N2 | codec 非法字符 | `codec="PCMA!"` | 400 `CODEC_INVALID` | | VGW-004-N3 | codec 重复 | 两条 `PCMA` | 400 `CODEC_DUPLICATE` | | VGW-004-N4 | 优先级重复 | 两条 priority=1 | 400 `CODEC_PRIORITY_DUPLICATE` | #### VGW-005 被叫前缀与主被叫改写 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证落地网关被叫前缀要求、主叫/被叫前缀改写规则和真实出局号码。 | | 前置条件 | 落地网关已绑定线路组,T/UAS 可接收并记录 INVITE。 | | 测试数据 | `landingCalleePrefix=86`;`prefixRules=[{direction:"CALLEE",matchPrefix:"00",replacePrefix:"+",priority:1},{direction:"CALLER",matchPrefix:"021",replacePrefix:"",priority:1}]`。 | | 步骤 | 1. 更新落地网关前缀和改写规则。2. 发布配置。3. 发起被叫需转换的呼叫。4. 在落地侧抓取 INVITE 或检查 T 日志。5. 查询 CDR 中落地号码快照。 | | 预期结果 | A 发往落地侧的 Request-URI/To 被叫符合 `landingCalleePrefix` 和 `CALLEE` 改写规则;主叫符合 `CALLER` 改写规则;CDR 保留原始号码和改写后号码快照。 | | 数据检查 | Redis `vendor_gateway:{id}` 包含 prefixRules;同 direction 下 priority 唯一。 | | 安全检查 | 非法前缀字符或重复优先级返回错误,不发布。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | VGW-005-N1 | 非法 landingCalleePrefix | `86+` 或中文 | 400 `LANDING_CALLEE_PREFIX_INVALID` | | VGW-005-N2 | prefixRules 非数组 | `prefixRules={}` | 400 `PREFIX_RULES_INVALID` | | VGW-005-N3 | direction 非法 | `direction=BOTH` | 400 `PREFIX_DIRECTION_INVALID` | | VGW-005-N4 | matchPrefix 非法 | 含空格或中文 | 400 `PREFIX_INVALID` | | VGW-005-N5 | 同方向优先级重复 | 两条 `CALLEE` priority=1 | 400 `PREFIX_RULE_PRIORITY_DUPLICATE` | #### VGW-006 指定主叫号码池 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证落地网关可配置主叫改写号码池,呼叫时按权重/状态选择出局主叫。 | | 前置条件 | 落地网关启用并参与线路组。 | | 测试数据 | `callerRewritePool=[{caller:"02160010001",weight:80},{caller:"02160010002",weight:20},{caller:"02160010003",weight:10,status:"DISABLED"}]`。 | | 步骤 | 1. 更新主叫号码池。2. 发布配置。3. 连续发起多通呼叫。4. 统计落地侧收到的 From/P-Asserted-Identity 或系统实际使用字段。 | | 预期结果 | 只使用启用号码;权重越高被选择比例越高;禁用号码不被使用;无号码池时按默认主叫规则。 | | 数据检查 | Redis `vendor_gateway:{id}:caller_rewrite_pool` 仅包含启用号码;manifest `callerRewriteCount` 正确。 | | 安全检查 | 号码池最多 50 条,重复 caller 被拒绝。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | VGW-006-N1 | 非数组 | `callerRewritePool={}` | 400 `CALLER_REWRITE_POOL_INVALID` | | VGW-006-N2 | 数量超限 | 超过 50 条 | 400 `CALLER_REWRITE_POOL_TOO_LARGE` | | VGW-006-N3 | caller 非法 | 含 `+`、空格或中文 | 400 `CALLER_REWRITE_INVALID` | | VGW-006-N4 | 权重非法 | `weight=0` 或超 100000 | 400 `INTEGER_INVALID` | | VGW-006-N5 | caller 重复 | 两条相同 caller | 400 `CALLER_REWRITE_DUPLICATE` | #### VGW-007 落地网关启停、删除与发布 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证落地网关启用、禁用、软删除会影响线路组可选成员和呼叫选路。 | | 前置条件 | `vgw_primary_001` 和 `vgw_backup_001` 均在线并已发布。 | | 测试数据 | 主备两个落地网关 ID。 | | 步骤 | 1. 禁用 primary。2. 发布配置并发起呼叫。3. 启用 primary 后发布并发起呼叫。4. 删除未被保护的落地网关并发布。 | | 预期结果 | primary 禁用后呼叫转 backup;恢复后按优先级选择 primary;删除后快照不包含该网关可用路由;列表默认不展示软删除记录。 | | 数据检查 | 启停/删除生成 `vendor_gateway_config` outbox 和审计;历史 CDR 成本快照不被破坏。 | | 安全检查 | 无 `vendor_gateways.manage` 返回 403;被线路组引用的网关删除行为按产品规则受控。 | #### LLG-001 创建线路组并添加成员 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证线路组创建、成员添加、并发汇总和配置发布。 | | 前置条件 | 至少存在两个启用落地网关。 | | 测试数据 | 线路组 `name=Auto Line Group LLG001`;成员 A:`vendorGatewayId=vgw_primary_001`、`priority=1`、`weight=2`、`concurrencyCap=60`;成员 B:`vendorGatewayId=vgw_backup_001`、`priority=2`、`weight=1`、`concurrencyCap=40`。 | | 步骤 | 1. 请求 `POST /api/v2/landing-line-groups`。2. 添加两个成员。3. 请求详情和列表。4. 发布配置并检查 Redis。 | | 预期结果 | 线路组创建成功;`itemCount=2`;`enabledItemCount=2`;`concurrencyCapSum=100`;`gatewayConcurrencyLimitSum` 等于启用成员网关并发限制合计;Redis 写入 `line_group:{id}` 和 `line_group:{id}:items`。 | | 数据检查 | 每次写操作生成 `line_group_config` outbox;审计包含 `create`、`item_create`。 | | 安全检查 | 只读用户可查看不可写,写返回 403。 | #### LLG-002 线路组成员更新与重排 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证线路组成员优先级、权重、并发上限和状态影响路由顺序。 | | 前置条件 | 线路组 `llg_vendor_seed_001` 含 primary 和 backup 两个成员。 | | 测试数据 | 将 backup 重排到第一位;将 primary `status=DISABLED`;调整 `concurrencyCap=20`、`weight=5`。 | | 步骤 | 1. 请求 `POST /api/v2/landing-line-groups/:id/items/reorder`。2. PATCH 更新成员状态/权重/并发。3. 发布配置。4. 发起呼叫确认选路。 | | 预期结果 | 详情中成员顺序按新 priority;禁用成员不计入 `enabledItemCount` 和并发汇总;呼叫按新顺序和状态选路。 | | 数据检查 | Redis `line_group:{id}:items` 顺序正确;生成 `item_update`、`item_reorder` 审计和 outbox。 | | 安全检查 | 重排必须使用同一线路组完整成员集合或按产品规则拒绝。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | LLG-002-N1 | 权重非法 | `weight=0` 或 `1001` | 400 `INTEGER_INVALID` | | LLG-002-N2 | 并发上限非法 | `concurrencyCap=-1` 或 `100001` | 400 `INTEGER_INVALID` | | LLG-002-N3 | priority 非法 | `priority=0` 或 `10001` | 400 `INTEGER_INVALID` | | LLG-002-N4 | 重排空列表 | `itemIds=[]` | 400 `LINE_GROUP_ITEM_IDS_INVALID` | | LLG-002-N5 | 删除成员 | `DELETE /landing-line-groups/:id/items/:gatewayId` | 成员移除,汇总和发布快照同步更新 | #### LLG-003 删除被引用线路组 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证线路组被客户网关或策略引用时不能破坏性删除。 | | 前置条件 | 客户网关 `cgw_ip_seed_001` 或策略引用 `llg_vendor_seed_001`。 | | 测试数据 | 被引用线路组 ID;未引用线路组 ID。 | | 步骤 | 1. 删除被引用线路组。2. 删除未引用线路组。3. 查询客户网关/策略引用关系和配置快照。 | | 预期结果 | 被引用线路组删除被拒绝或只能禁用,规则固定;未引用线路组可软删除;不会产生悬空 `lineGroupId`。 | | 数据检查 | 删除成功设置软删除标记;被拒绝时不修改引用;审计和 outbox 按结果记录。 | | 安全检查 | 无权限删除返回 403;错误不泄露内部约束名。 | #### LLG-004 真实呼叫选路与故障转移 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证客户策略指向线路组后,OpenSIPS 能按线路组成员优先级、状态、禁呼和并发选择落地网关。 | | 前置条件 | A/B/T 联调环境 active;客户网关策略指向 `llg_vendor_seed_001`;primary 和 backup 均可被 T/UAS 验证。 | | 测试数据 | 一通正常呼叫;一组 primary 禁用或禁呼时段内呼叫;一组并发接近上限呼叫。 | | 步骤 | 1. 发布当前配置。2. 发起正常呼叫,确认落到 primary。3. 禁用 primary 或设置当前禁呼,发布后再呼叫。4. 配置 primary 并发上限较低,发起多路并发。5. 查询 CDR、录音和落地网关成本。 | | 预期结果 | 正常呼叫选 primary;primary 不可用时转 backup;并发达到上限后新呼叫转备用或被拒绝;CDR 记录实际落地网关、供应商成本、失败原因。 | | 数据检查 | `raw_cdrs/rated_cdrs` 中 vendorGatewayId、lineGroupId、cost 字段正确;HOMER 可看到出局 INVITE 目标。 | | 安全检查 | 故障转移不绕过客户鉴权、余额检查、业务前缀和主叫限制。 | 字段校验矩阵: | 对象 | 字段 | 合法规则 | 非法示例 | 期望错误 | | --- | --- | --- | --- | --- | | 供应商 | `name` | 必填,1-120 字符,唯一 | 空、超长、重复 | `VALIDATION_ERROR` 或 409 | | 供应商 | `creditLimit` | 非负,最多 14 位整数、6 位小数 | `-1`、`1.1234567` | `MONEY_INVALID` | | 落地网关 | `authMode` | `IP`、`SIP_DIGEST`、`MIXED` | `PASSWORD` | `AUTH_MODE_INVALID` | | 落地网关 | `host` | IP 或小写 hostname 规则 | `bad_host!` | `HOST_INVALID` | | 落地网关 | `port` | 1-65535 整数 | `0`、`65536` | `INTEGER_INVALID` | | 落地网关 | `transport` | `udp`、`tcp`、`tls` | `ws` | `TRANSPORT_INVALID` | | 落地网关 | `sipUsername` | SIP/MIXED 必填,安全字符 | `carrier user` | `SIP_USERNAME_INVALID` | | 落地网关 | `sipPassword` | SIP/MIXED 创建或身份变更必填,至少 12 字符 | `short` | `SIP_PASSWORD_WEAK`、`SIP_PASSWORD_REQUIRED` | | 落地网关 | `cpsLimit` | 0-10000 整数,0 表示不限 | `-1`、`10001` | `INTEGER_INVALID` | | 落地网关 | `concurrencyLimit` | 0-100000 整数,0 表示不限 | `-1`、`100001` | `INTEGER_INVALID` | | 落地网关 | `billingCycleSec` | 1-60 整数 | `0`、`61` | `INTEGER_INVALID` | | 落地网关 | `cycleRate` | 非负,最多 14 位整数、6 位小数 | `-1`、`1.1234567` | `MONEY_INVALID` | | 落地网关 | `landingCalleePrefix` | 可空,字母数字 | `86+` | `LANDING_CALLEE_PREFIX_INVALID` | | 落地网关 | `forbiddenPeriods` | 数组,weekdayMask 1-127,时间 HH:mm:ss | 非数组、`24:00:00` | `FORBIDDEN_PERIODS_INVALID`、`TIME_INVALID` | | 落地网关 | `codecs` | 数组,codec/priority 唯一 | 重复 codec 或 priority | `CODEC_DUPLICATE`、`CODEC_PRIORITY_DUPLICATE` | | 落地网关 | `prefixRules` | 数组,direction 为 CALLER/CALLEE,同方向 priority 唯一 | `BOTH`、重复优先级 | `PREFIX_DIRECTION_INVALID`、`PREFIX_RULE_PRIORITY_DUPLICATE` | | 落地网关 | `callerRewritePool` | 最多 50 条,caller 唯一,weight 1-100000 | 重复 caller、weight=0 | `CALLER_REWRITE_DUPLICATE`、`INTEGER_INVALID` | | 线路组 | `name` | 必填,1-120 字符 | 空、超长 | `VALIDATION_ERROR` | | 线路组成员 | `priority` | 1-10000 整数 | `0`、`10001` | `INTEGER_INVALID` | | 线路组成员 | `weight` | 1-1000 整数 | `0`、`1001` | `INTEGER_INVALID` | | 线路组成员 | `concurrencyCap` | 0-100000 整数,0 表示不限 | `-1`、`100001` | `INTEGER_INVALID` | ### 8.5 SIP 呼叫、CDR 与计费 本节覆盖客户接入、SIP 注册/鉴权、业务前缀识别、线路组出局、Redis Stream CDR、CDR Worker 计费、客户余额扣减、失败话单、幂等、号码归属地和话单查询。三机端到端用例默认在 A/B/T 联调环境执行。 公共前置数据: - 客户 `cus_call_seed_001`:状态 `ENABLED`,`billingMode=PREPAID`,余额充足,授信和最低余额按测试场景设置。 - IP 客户网关 `cgw_call_ip_001`:`authMode=IP`,T 源 IP 已在 `sourceIps`,启用,绑定业务前缀 `671` 和线路组 `llg_call_seed_001`。 - SIP 客户网关 `cgw_call_sip_001`:`authMode=SIP_DIGEST`,账号密码已知,启用,绑定线路组。 - 线路组 `llg_call_seed_001`:启用,包含 `vgw_call_primary_001` 和 `vgw_call_backup_001`。 - 落地网关 `vgw_call_primary_001`:启用,计费周期 `6` 秒,费率 `0.012000`,T/UAS 可接收。 - 业务前缀 `bp_671_seed`:`prefix=671`,启用。 - 号码库包含 `1380013` 号段,归属地和运营商字段可预测。 - Config Publisher 已成功发布,A 侧 OpenSIPS 使用最新 `cfg:active_version`。 #### CALL-001 IP 客户成功呼叫 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 IP 鉴权客户从 T 发起呼叫时,A 能识别客户网关、匹配业务前缀、选择线路组落地,并生成完整 CDR。 | | 前置条件 | A/B/T 服务 active;`cgw_call_ip_001` 启用;T 源 IP 位于网关 `sourceIps`;落地 UAS 可接听。 | | 测试数据 | 主叫 `02160010001`;被叫 `67113800138000`;预期真实被叫 `13800138000`;Call-ID 使用唯一值 `call-001-{timestamp}`。 | | 步骤 | 1. 从 T 使用 IP 客户脚本发起呼叫。2. 等待 100 Trying/Giving it a try、180/183 或 200 OK。3. 持话 6-10 秒后 BYE。4. 在 B 查询 Redis Stream、raw CDR、rated CDR、录音记录。5. 在 HOMER 查询 Call-ID。 | | 预期结果 | SIP 返回 100 和 200 OK,BYE 返回 200 OK;A 出局到预期落地网关;B 生成 1 条 raw CDR 和 1 条 rated CDR;CDR 包含 customerId、customerGatewayId、lineGroupId、vendorGatewayId、businessPrefix、rawCallee、callee、landingCaller、landingCallee。 | | 数据检查 | raw CDR `ratingStatus=RATED`;rated CDR `billSec`、费用和余额扣减正确;`configVersion` 为当前 active version;HOMER 中可查到入局和出局信令。 | | 安全检查 | 非白名单源 IP 不应复用该客户网关;CDR payload 不包含 SIP 密码或内部密钥。 | #### CALL-002 SIP Digest 注册和呼叫成功 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 SIP Digest 客户可使用正确账号注册/鉴权并完成呼叫。 | | 前置条件 | `cgw_call_sip_001` 启用;SIP 用户名、域、密码与 A 热路径 HA1 一致;T 脚本支持 REGISTER/INVITE Digest。 | | 测试数据 | SIP 用户名 `sip-cus-001`;域 `sip.example.local`;正确密码;被叫 `67113800138000`。 | | 步骤 | 1. T 使用正确账号执行 REGISTER。2. 使用同一账号发起 INVITE。3. 持话后挂断。4. 查询 CDR 和客户网关识别结果。 | | 预期结果 | REGISTER 成功或 INVITE Digest 鉴权成功;呼叫成功;CDR 归属到 `cgw_call_sip_001`;source_ip 记录 T IP;业务前缀和落地路由正常。 | | 数据检查 | raw/rated CDR 各 1 条;客户余额扣减一次;HOMER 可看到 401 challenge 后的成功鉴权流程。 | | 安全检查 | CDR、日志、审计不记录明文 SIP 密码。 | #### CALL-003 SIP Digest 密码错误 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证错误 SIP 密码不能注册或呼叫,且不会产生成功计费。 | | 前置条件 | SIP 网关启用。 | | 测试数据 | 正确用户名和域;错误密码 `WrongDigestPassword-003`。 | | 步骤 | 1. T 使用错误密码执行 REGISTER。2. 使用错误密码发起 INVITE。3. 查询 A 日志、HOMER、CDR、客户余额。 | | 预期结果 | REGISTER/INVITE 被 401/403 拒绝或无成功路由;不产生 rated CDR;客户余额不变;如产生失败 raw CDR,应标记 `SKIPPED` 且费用为 0。 | | 数据检查 | 没有成功通话录音;失败原因可追踪;不产生余额扣减。 | | 安全检查 | 响应不暴露正确 HA1、密码规则或账号存在性细节。 | #### CALL-004 客户余额不足或低于最低余额 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证预付费客户余额不足时,热路径阻止新呼叫或按授信/最低余额规则保护。 | | 前置条件 | 客户 `cus_low_balance_001` 预付费,余额、授信、最低余额已设置到触发保护条件;配置已发布。 | | 测试数据 | 余额 `0.000000` 或低于 `minBalance`;被叫 `67113800138000`。 | | 步骤 | 1. 发起呼叫。2. 查询 SIP 响应。3. 查询 raw/rated CDR、客户余额和 A 日志。 | | 预期结果 | 呼叫被拒绝,建议返回明确 402/403/503 类业务失败响应,具体以 OpenSIPS 配置为准;不产生 rated CDR;余额不被错误扣减。 | | 数据检查 | 如生成 raw CDR,`ratingStatus=SKIPPED`,`customerFee=0`;失败原因包含余额不足或策略拒绝。 | | 安全检查 | 余额不足响应不泄露客户余额具体值给 SIP 对端。 | #### CALL-005 禁用客户、客户网关或业务前缀呼叫 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证禁用状态会进入热路径并阻止呼叫。 | | 前置条件 | 分别准备禁用客户、禁用客户网关、禁用业务前缀三种配置,并发布。 | | 测试数据 | 同一主叫和被叫 `67113800138000`。 | | 步骤 | 1. 禁用客户后发布并呼叫。2. 恢复客户,禁用客户网关后发布并呼叫。3. 恢复网关,禁用业务前缀后发布并呼叫。4. 查询 CDR 和日志。 | | 预期结果 | 三种禁用均导致呼叫被拒绝或无法匹配;不产生成功 rated CDR;失败原因可追踪。 | | 数据检查 | Redis active snapshot 不包含禁用业务前缀;禁用网关不应作为可用入口;客户余额不变。 | | 安全检查 | 禁用对象恢复后必须重新发布才能生效,不应使用陈旧配置误放行。 | #### CALL-006 落地网关禁用故障转移 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证优先级最高落地网关不可用时,线路组可转到下一可用成员。 | | 前置条件 | 线路组包含 primary 和 backup;两者都可被 T/UAS 接听。 | | 测试数据 | 禁用 `vgw_call_primary_001`;backup `vgw_call_backup_001` 启用。 | | 步骤 | 1. 正常呼叫确认 primary 被选中。2. 禁用 primary 并发布。3. 再次呼叫。4. 查询 CDR、HOMER 和落地侧日志。 | | 预期结果 | 禁用前 CDR 记录 primary;禁用后新呼叫记录 backup;SIP 呼叫成功;供应商成本按 backup 费率计算。 | | 数据检查 | CDR `vendorGatewayId`、`vendorId`、`lineGroupId` 正确;配置版本变化可追踪。 | | 安全检查 | 故障转移不绕过客户鉴权、主叫匹配、业务前缀匹配和余额保护。 | #### CALL-007 主叫/被叫策略不匹配 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证客户网关主叫前缀、业务前缀和策略匹配失败时不会误路由。 | | 前置条件 | 客户网关设置 `callerMatchMode=PREFIXES` 且只允许 `021`;`calleeMatchMode=BUSINESS_PREFIXES` 只允许 `671`;策略设置特定主被叫。 | | 测试数据 | 主叫 `01060010001`;被叫 `67213800138000` 或不满足策略的号码。 | | 步骤 | 1. 发起主叫不匹配呼叫。2. 发起业务前缀不匹配呼叫。3. 发起策略不匹配呼叫。4. 查询 CDR 和日志。 | | 预期结果 | 呼叫被拒绝或进入明确失败路径;不产生 rated CDR;raw CDR 如存在,placeholder ID 归一为 null,失败原因可追踪。 | | 数据检查 | `customer_gateway_policy_id=no_policy_match` 等占位值入库时转换为 null;余额不扣减。 | | 安全检查 | 不匹配失败不暴露可用前缀或策略详情。 | #### CDR-001 成功话单计费 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证成功 2xx 通话按落地网关计费周期向上取整,并扣减客户余额。 | | 前置条件 | 落地网关 `billingCycleSec=6`,`cycleRate=0.012000`;客户余额 `10.000000`。 | | 测试数据 | CDR `duration_sec=28`,`sip_code=200`。 | | 步骤 | 1. 通过真实呼叫或注入 CDR Stream 生成 28 秒成功事件。2. 等待 CDR Worker 消费。3. 查询 raw/rated CDR 和客户余额。 | | 预期结果 | `cycles=5`;`billSec=30`;`customerFee=0.060000`;`vendorCost=0.060000`;`grossProfit=0.000000`;客户余额变为 `9.940000`。 | | 数据检查 | raw CDR `ratingStatus=RATED`;rated CDR `customerRate.source=S23_MINIMAL_MIRROR_VENDOR_RATE`,`vendorRate.source=vendor_gateway`;客户 `updatedBy=worker-cdr`,version 增加。 | | 安全检查 | 计费不写充值流水;余额扣减在数据库事务内完成。 | 计费边界矩阵: | durationSec | billingCycleSec | cycleRate | 期望 billSec | 期望金额 | | --- | --- | --- | --- | --- | | 1 | 6 | 0.012000 | 6 | 0.012000 | | 6 | 6 | 0.012000 | 6 | 0.012000 | | 7 | 6 | 0.012000 | 12 | 0.024000 | | 28 | 6 | 0.012000 | 30 | 0.060000 | | 60 | 60 | 0.120000 | 60 | 0.120000 | #### CDR-002 失败、拒接和零时长话单 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证非 2xx 或零时长事件只落 raw CDR,不扣余额。 | | 前置条件 | CDR Worker active;客户余额已知。 | | 测试数据 | `sip_code=486`、`503`、`404`;`duration_sec=0`;`answered_at=none`。 | | 步骤 | 1. 模拟忙线、拒接、无路由或注入失败 CDR。2. 等待 Worker 消费。3. 查询 raw/rated CDR 和余额。 | | 预期结果 | raw CDR 创建成功;`ratingStatus=SKIPPED`;不创建 rated CDR 或 rated 金额为 0,按实现固定;客户余额不变。 | | 数据检查 | `hangupReason`、`sipCode`、`startedAt/endedAt` 保存;失败 CDR 可在话单中心筛选。 | | 安全检查 | 失败话单不触发录音播放越权,不泄露内部路由细节。 | #### CDR-003 重复 CDR 幂等 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证同一 `event_id` 重复进入 Redis Stream 时,raw/rated CDR 和余额扣减只发生一次。 | | 前置条件 | Redis Stream 和 CDR Worker active。 | | 测试数据 | 两条完全相同 CDR,`event_id=evt-cdr-003-001`,`duration_sec=28`,`sip_code=200`。 | | 步骤 | 1. 向 `stream:cdr_payload` 写入第一条事件。2. 写入重复事件。3. 等待 Worker 消费。4. 查询 raw CDR、rated CDR、客户余额、Redis ack/lock。 | | 预期结果 | 只有 1 条 raw CDR 和 1 条 rated CDR;第二条处理结果为 duplicate 或被 Redis 幂等锁 ACK;客户余额只扣一次。 | | 数据检查 | `raw_cdrs.event_id` 唯一;`rated_cdrs.raw_cdr_id` 唯一;Redis `lock:cdr:{event_id}` 在 TTL 内存在。 | | 安全检查 | 重复事件不进入 deadletter;重复处理不产生告警噪音。 | #### CDR-004 CDR Stream 格式校验和死信 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证非法 CDR Stream payload 不会阻塞队列,并进入 deadletter。 | | 前置条件 | CDR consumer group 已创建。 | | 测试数据 | 缺少 `event_id`;`schema_version=999`;`duration_sec=-1`;`sip_code=999`。 | | 步骤 | 1. 向 Redis Stream 写入非法事件。2. 执行 CDR Worker 消费。3. 查询 deadletter stream、ACK 和主 stream pending。 | | 预期结果 | 非法事件写入 `stream:cdr_payload:deadletter` 或配置的 deadletter stream;原消息 ACK;主消费组 pending 不增长。 | | 数据检查 | deadletter 包含 `original_redis_id`、`event_id`、`call_id`、`error`、`payload`、`deadlettered_at`。 | | 安全检查 | deadletter payload 不包含密码或访问令牌;错误信息截断且可读。 | #### CDR-005 Worker 可恢复错误重试 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 MySQL 短故障等可恢复错误不会 ACK 原消息,也不会误入 deadletter。 | | 前置条件 | CDR Worker 正常;可临时停止 MySQL 或注入 `CdrRetryableError`。 | | 测试数据 | 一条成功呼叫 CDR。 | | 步骤 | 1. 停止 MySQL 或制造数据库连接失败。2. 写入 CDR 事件并执行 Worker。3. 检查 Redis pending 和 deadletter。4. 恢复 MySQL。5. 执行 pending reclaim/重试。 | | 预期结果 | 故障期间消息保留 pending,幂等锁释放;不 ACK、不 deadletter;恢复后重试成功,raw/rated CDR 入库。 | | 数据检查 | pending 从 1 回到 0;deadletter 无新增;余额只扣一次。 | | 安全检查 | 可恢复错误日志不包含数据库密码。 | #### CDR-006 缺失计费对象 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证 CDR 中缺少客户或落地网关等关键计费对象时,系统不会错误扣费。 | | 前置条件 | CDR Worker active。 | | 测试数据 | `customer_id=unknown`、`vendor_gateway_id=unknown`、`customer_gateway_policy_id=no_policy_match`、`vendor_gateway_id` 指向不存在网关。 | | 步骤 | 1. 分别注入上述 CDR。2. 等待 Worker 消费。3. 查询 raw/rated CDR 和余额。 | | 预期结果 | placeholder ID 入库时归一为 null;缺少 billable customer/vendor gateway 时 raw 标记 `SKIPPED` 或 `FAILED`,按实现固定;不扣余额。 | | 数据检查 | `none`、`unknown`、`no_active_version`、`no_policy_match`、`single_gateway` 等占位不作为外键写入;失败原因可定位。 | | 安全检查 | 不因无效外键导致 Worker 崩溃或队列停滞。 | #### CDR-007 号码归属地识别 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证号码库快照参与呼叫归属地和运营商识别,并写入 CDR。 | | 前置条件 | 号码库已导入并发布;包含手机号段 `1380013` 和区号/运营商规则。 | | 测试数据 | 被叫 `67113800138000`;预期 `normalized_callee=13800138000`。 | | 步骤 | 1. 发起成功呼叫。2. 查询 raw CDR payload 和话单列表。3. 使用 `cityCode`、`carrier` 筛选话单。 | | 预期结果 | CDR 包含 `calleeCityCode`、`calleeCityName`、`calleeProvinceName`、`calleeOperator`、`calleeNumberType`;话单筛选能命中该记录。 | | 数据检查 | 手机号、固话、国际号码分别识别为 `MOBILE`、`LANDLINE`、`INTERNATIONAL` 或 `UNKNOWN`;未知号码不应报错。 | | 安全检查 | 号码库未命中时降级为 UNKNOWN,不影响话单入库。 | #### CDR-008 话单查询和详情 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证话单中心 API 查询条件、分页、详情和权限。 | | 前置条件 | 已存在多条不同主叫、被叫、客户网关、落地网关、城市、运营商和时间的话单。 | | 测试数据 | `caller=02160010001`,`callee=13800138000`,`customerGatewayId=cgw_call_ip_001`,`vendorGatewayId=vgw_call_primary_001`,`cityCode=340100`,`carrier=MOBILE`,`take=20`,`skip=0`。 | | 步骤 | 1. 使用有 `cdr.view` 权限账号请求 `GET /api/v2/cdrs`。2. 分别组合号码、网关、城市、运营商和时间范围查询。3. 请求 `GET /api/v2/cdrs/:id`。4. 使用无权限账号请求。 | | 预期结果 | 有权限查询返回匹配数据和分页信息;详情包含 raw/rated、录音、业务前缀、落地、费用等字段;无权限返回 403。 | | 数据检查 | 查询默认 `take=100`,最大 500;`skip` 最大 1000000;时间范围按 startedAt 筛选。 | | 安全检查 | 非法查询不触发 SQL 注入;无权限账号不能看到话单。 | 补充负向用例: | 子 ID | 场景 | 步骤 | 期望结果 | | --- | --- | --- | --- | | CDR-008-N1 | 非法运营商 | `carrier=CMCC` | 400 `CARRIER_INVALID` | | CDR-008-N2 | 时间范围反转 | `startedFrom > startedTo` | 400 `TIME_RANGE_INVALID` | | CDR-008-N3 | 非法日期 | `startedFrom=not-a-date` | 400 `TIME_RANGE_INVALID` | | CDR-008-N4 | 分页非法 | `take=0`、`take=501`、`skip=-1` | 400 `QUERY_INVALID` | | CDR-008-N5 | 查询字段超长 | caller/callee 超 64 或 gatewayId 超 32 | 400 `QUERY_INVALID` | #### CDR-009 长 event_id 存储兼容 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证超过数据库 `event_id` 长度限制的 CDR event_id 会稳定压缩存储,不导致入库失败。 | | 前置条件 | CDR Worker active。 | | 测试数据 | 长度超过 64 的 `event_id`,其余字段为成功计费事件。 | | 步骤 | 1. 注入长 event_id CDR。2. 等待 Worker 消费。3. 查询 raw CDR `eventId` 和 payload。4. 重复注入相同长 event_id。 | | 预期结果 | raw CDR 入库成功;存储 eventId 被稳定截断加 hash 后缀;payload 保留原始 event_id;重复事件仍按同一 event_id 幂等处理。 | | 数据检查 | 不进入 deadletter;余额只扣一次。 | | 安全检查 | hash 截断逻辑稳定,不泄露额外敏感信息。 | #### CDR-010 并发计费余额一致性 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证多条成功 CDR 并发处理时,客户余额通过事务锁串行扣减,不丢扣、不重复扣。 | | 前置条件 | 客户余额足够;CDR Worker 可并发或批量消费。 | | 测试数据 | 同一客户 5-12 条不同 `event_id` 成功 CDR,每条预期费用 `0.012000` 或固定值。 | | 步骤 | 1. 批量写入多条 CDR。2. 启动 Worker 消费。3. 查询 rated CDR 数量和客户余额。4. 检查 Redis pending/deadletter。 | | 预期结果 | rated CDR 数量等于成功事件数;余额减少等于费用总和;pending=0;deadletter=0。 | | 数据检查 | 每条 raw CDR event_id 唯一;客户 version 增加符合更新次数或按实现固定。 | | 安全检查 | 并发下不产生负外键、重复 rated 或事务死锁长时间阻塞。 | ### 8.6 录音、播放与质检 本节覆盖 RTPEngine 录音 ready 文件、Recording Worker 搬运、校验、源文件清理、录音播放鉴权、Nginx X-Accel/Range、质检抽样、评分保存和审计。后端接口统一以 `/api/v2` 为前缀。 公共前置数据: - 录音查看账号 `recording_viewer_001`:包含 `cdr.view`、`recordings.play`。 - 质检账号 `quality_ops_001`:包含 `quality.view`、`quality.manage`、`recordings.play`。 - 只读质检账号 `quality_viewer_001`:包含 `quality.view`,不包含 `quality.manage`、`recordings.play`。 - 至少 1 条成功通话 CDR,关联 READY 录音 `rec_ready_seed_001`,storageKey 指向 B 本地录音数据盘。 - 至少 1 条 PENDING 录音 `rec_pending_seed_001`,用于播放和质检不可用状态。 - A 侧 `/dev/shm/voip_rec` 或测试目录可生成 ready 文件,B 侧录音存储目录可写。 #### REC-001 录音搬运成功 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 Recording Worker 从 A 拉取 ready 文件,校验大小/hash 后写入 B,并安全删除源文件。 | | 前置条件 | A 侧存在测试录音文件和 ready 标记;B 侧 Recording Worker 可通过私网访问 A;关联 CDR/callId 可匹配。 | | 测试数据 | 文件名包含 Call-ID;ready 内容包含相对路径和 size;文件大小固定,例如 55758 bytes。 | | 步骤 | 1. 在 A 侧生成测试录音和 ready 文件。2. 启动或触发 Recording Worker。3. 查询 B 侧录音文件、数据库 recordings 表和 Worker 日志。4. 检查 A 源文件和 ready 文件。 | | 预期结果 | B 侧文件存在,大小/hash 与 A 源文件一致;recording 状态为 `READY`;storageKey 为安全相对路径;A 源文件在校验成功后删除;日志记录成功搬运。 | | 数据检查 | recordings 记录包含 callId、rawCdrId 或关联键、fileSize、checksum、storageKey、readyAt;状态从 PENDING 变为 READY。 | | 安全检查 | Worker 只处理配置目录下文件;不会跟随任意绝对路径或 `../` 路径。 | #### REC-002 本地校验失败保留源文件 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证复制后大小/hash 不一致时不删除 A 源文件,避免录音丢失。 | | 前置条件 | 可构造错误 ready size、截断文件或模拟 B 写入失败。 | | 测试数据 | ready size 与实际文件大小不一致。 | | 步骤 | 1. 生成 size 不一致的 ready 文件。2. 执行 Recording Worker。3. 查询 A/B 文件和 recordings 状态。 | | 预期结果 | Worker 标记失败或保持待重试;A 源文件保留;B 不产生 READY 记录;错误日志可定位。 | | 数据检查 | recordings 不应被误标记 READY;如生成 FAILED 状态,应记录失败原因和重试信息。 | | 安全检查 | 失败日志不泄露 SSH 私钥、完整内部路径或连接凭据。 | #### REC-003 播放接口鉴权与权限 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证录音播放必须登录且具备播放权限。 | | 前置条件 | 存在 READY 录音 `rec_ready_seed_001`。 | | 测试数据 | 未登录请求;有 `quality.view` 但无 `recordings.play` 的账号;有 `recordings.play` 的账号。 | | 步骤 | 1. 未登录请求录音播放接口。2. 使用 `quality_viewer_001` 请求播放。3. 使用 `recording_viewer_001` 请求播放。 | | 预期结果 | 未登录返回 401 `AUTH_REQUIRED`;无播放权限返回 403;有权限返回 200/206 或 X-Accel 跳转响应,按部署方式固定。 | | 数据检查 | 无权限请求不写播放成功审计;有权限播放可按审计策略记录访问。 | | 安全检查 | 不允许通过猜 ID 或 storageKey 绕过权限。 | #### REC-004 录音路径穿越防护 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证录音 storageKey 和播放接口防止路径穿越、绝对路径和特殊字符读取。 | | 前置条件 | 有权限账号已登录。 | | 测试数据 | 伪造 ID 或 storageKey:`../etc/passwd`、`..\\windows\\win.ini`、`/etc/passwd`、空值、超长 key。 | | 步骤 | 1. 直接请求伪造录音 ID。2. 构造数据库中异常 storageKey 的测试记录并请求播放。3. 检查响应和日志。 | | 预期结果 | 返回 404 或安全错误;不返回目标文件内容;Nginx internal path 不被构造到录音根目录之外。 | | 数据检查 | 安全拒绝不改变 recording 状态。 | | 安全检查 | 响应不泄露服务器真实路径和目录结构。 | #### REC-005 Range 播放和浏览器播放 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证录音支持浏览器播放、Range 请求和常见播放器拖动。 | | 前置条件 | READY 录音文件存在,Nginx/X-Accel 配置已启用。 | | 测试数据 | `Range: bytes=0-1023`,完整播放请求,超出范围请求。 | | 步骤 | 1. 使用浏览器点击播放。2. 使用 curl 请求 Range。3. 请求完整文件。4. 请求超出文件大小的 Range。 | | 预期结果 | 合法 Range 返回 206,`Content-Range` 和 `Content-Length` 正确;完整请求返回 200 或内部跳转;浏览器可播放;超范围返回 416 或合理错误。 | | 数据检查 | 播放不改变录音文件;访问日志可追踪 recordingId。 | | 安全检查 | 响应头不暴露真实磁盘路径;只允许内部 X-Accel 路径。 | #### QA-001 稳定抽样 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证同一抽样规则和录音在多次刷新中抽样结果稳定。 | | 前置条件 | 存在多个 READY 录音;有启用抽样规则。 | | 测试数据 | 抽样比例 0%、10%、100%;按客户或全局规则。 | | 步骤 | 1. 创建或更新抽样规则。2. 多次请求质检列表。3. 调整规则 ID 或比例后再次请求。 | | 预期结果 | 同一规则下同一 recording 抽样命中结果稳定;0% 不命中,100% 全命中;规则变化后可重新计算。 | | 数据检查 | 列表返回 samplingScore、reviewStatus 或等价字段;不会随机抖动。 | | 安全检查 | 无 `quality.view` 不能查看质检列表。 | #### QA-002 保存质检评分 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证质检人员可保存评分、结果、标签和备注,并更新录音 review 状态。 | | 前置条件 | `quality_ops_001` 已登录;录音 READY 且可质检。 | | 测试数据 | `score=88.5`,`result=ISSUE`,`issueTags=["noise","script"]`,`remark=QA-002 review`。 | | 步骤 | 1. 打开质检中心并选择录音。2. 播放录音。3. 提交评分、结果、标签、备注。4. 刷新列表和详情。5. 查询审计。 | | 预期结果 | 保存成功;录音 review 状态更新;列表展示评分、结果、质检人和时间;审计写入 `quality` 模块成功记录。 | | 数据检查 | 重复保存按更新逻辑覆盖或产生历史版本,规则需固定;录音文件不被修改。 | | 安全检查 | 无 `quality.manage` 保存返回 403;无 `recordings.play` 不应播放但可否评分按产品规则固定。 | #### QA-003 评分边界校验 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证评分、结果枚举、标签和备注长度校验。 | | 前置条件 | 有可质检录音。 | | 测试数据 | 合法评分 `0`、`100`、`88.55`;非法评分 `-1`、`100.01`、`abc`;非法 result;超长备注。 | | 步骤 | 1. 分别提交合法和非法评分。2. 提交非法 result。3. 提交超长标签/备注。 | | 预期结果 | 合法评分保存成功;非法评分返回 400;非法 result 返回校验错误;超长字段被拒绝。 | | 数据检查 | 非法提交不改变上一条质检结果。 | | 安全检查 | 错误响应不包含堆栈。 | #### QA-004 质检列表筛选和连续播放 | 字段 | 内容 | | --- | --- | | 优先级 | P2 | | 目的 | 验证质检中心按客户、时间、结果、待质检状态筛选,并支持连续播放工作流。 | | 前置条件 | 多个客户、多条 READY 录音、部分已质检。 | | 测试数据 | 客户 ID、时间范围、`reviewStatus=PENDING/REVIEWED`。 | | 步骤 | 1. 按条件筛选录音。2. 打开第一条并播放。3. 保存评分后跳转下一条。4. 刷新列表。 | | 预期结果 | 筛选结果正确;连续播放不跳过未质检录音;保存后列表状态同步。 | | 数据检查 | 待质检数量减少;已质检数量增加。 | | 安全检查 | 无权限账号不能看到不该访问的录音。 | ### 8.7 Dashboard、当前通话与操作日志 本节覆盖 Dashboard 聚合、上海时区日界、趋势桶、当前通话 OpenSIPS MI、挂断安全、审计查询和敏感字段脱敏。 公共前置数据: - Dashboard 账号 `dashboard_viewer_001`:包含 `dashboard.view`。 - 当前通话账号 `active_calls_ops_001`:包含 `active_calls.view`、`active_calls.manage`。 - 审计账号 `audit_viewer_001`:包含 `audit.view`。 - 数据库中存在今天、昨天和跨上海时区边界的 CDR、录音、质检、充值和审计数据。 - A 侧 OpenSIPS MI 可访问,至少可模拟或产生 1 条进行中 Dialog。 #### DASH-001 今日指标聚合 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证 Dashboard 今日通话数、接通率、消费、成本、毛利、待质检等聚合正确。 | | 前置条件 | 存在当日成功、失败和跳过话单。 | | 测试数据 | 今日 10 通,其中 6 通成功;客户费用 12.000000,供应商成本 8.000000,待质检 3 条。 | | 步骤 | 1. 请求 Dashboard summary 接口或打开 Dashboard。2. 与 SQL 聚合结果对比。3. 刷新页面。 | | 预期结果 | 通话数、接通率、消费、成本、毛利、待质检与数据库一致;金额以 6 位小数或前端格式正确展示。 | | 数据检查 | 失败话单计入总呼叫但不计入成功费用;跳过话单费用为 0。 | | 安全检查 | 无 `dashboard.view` 返回 403 或菜单不可见。 | #### DASH-002 上海时区日界 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证“今日”按 Asia/Shanghai 计算,不按 UTC 错分。 | | 前置条件 | 构造 `2026-06-28 00:00:00 +08:00` 前后数据。 | | 测试数据 | UTC `2026-06-27T15:59:59Z` 和 `2026-06-27T16:00:00Z` 两条 CDR。 | | 步骤 | 1. 查询今日 Dashboard。2. 查询昨日 Dashboard 或手工 SQL 对比。 | | 预期结果 | `16:00:00Z` 之后的数据计入 2026-06-28;之前数据计入 2026-06-27。 | | 数据检查 | 趋势和 summary 使用同一时区边界。 | | 安全检查 | 不因服务器本地时区变化导致统计漂移。 | #### DASH-003 趋势桶固定 | 字段 | 内容 | | --- | --- | | 优先级 | P2 | | 目的 | 验证趋势图固定桶数量,空桶补 0,时间范围校验。 | | 前置条件 | 存在多日或多小时 CDR。 | | 测试数据 | 7 天趋势;某天无数据。 | | 步骤 | 1. 请求趋势接口。2. 检查桶数量、顺序和空桶。3. 请求非法范围。 | | 预期结果 | 返回固定桶数;空桶数值为 0;非法范围返回校验错误。 | | 数据检查 | 每个桶起止时间连续不重叠。 | | 安全检查 | 大范围查询有上限,避免拖垮数据库。 | #### ACT-001 当前通话列表 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证当前通话从 OpenSIPS Dialog 归一化后可展示主叫、被叫、客户 IP、落地 IP、时长和状态。 | | 前置条件 | A 侧存在进行中 Dialog 或使用 MI mock。 | | 测试数据 | 一通进行中呼叫,含 Contact、SDP、dialog ID。 | | 步骤 | 1. 发起长持话呼叫。2. 请求当前通话接口或打开页面。3. 对比 OpenSIPS MI 输出。 | | 预期结果 | 列表显示 callId/dialogId、caller、callee、sourceIp、landingIp、durationSec、startedAt;自动刷新不闪退。 | | 数据检查 | 解析 IPv4/IPv6、Contact 和 SDP 中 IP 正确;无 Dialog 时返回空列表。 | | 安全检查 | 无 `active_calls.view` 不可查看。 | #### ACT-002 安全挂断 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证有权限用户可通过 OpenSIPS MI 挂断进行中通话,并处理已结束竞态。 | | 前置条件 | 有进行中 Dialog;`active_calls_ops_001` 已登录。 | | 测试数据 | 安全 dialog ID。 | | 步骤 | 1. 点击挂断。2. 查询 SIP BYE 和当前通话列表。3. 对已结束 dialog 再次挂断。 | | 预期结果 | 正常挂断返回成功,Dialog 从列表消失;OpenSIPS operation failed 但通话已结束时前端显示合理成功/已结束状态。 | | 数据检查 | CDR 正常生成,hangupReason 可追踪。 | | 安全检查 | 无 `active_calls.manage` 不能挂断。 | #### ACT-003 非法 Dialog ID | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证挂断接口只接受安全 dialog ID,防止 MI 命令注入。 | | 前置条件 | 有权限账号。 | | 测试数据 | `../x`、`;rm -rf`、超长 ID、包含空格/换行。 | | 步骤 | 1. 调用挂断接口传非法 ID。2. 检查 OpenSIPS MI 是否被调用。 | | 预期结果 | 返回 400;不调用 MI;服务不崩溃。 | | 数据检查 | 无 CDR 或 Dialog 状态被误改。 | | 安全检查 | 错误不回显危险字符串到日志未脱敏区域。 | #### AUD-001 审计列表查询 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证操作日志按用户、模块、动作、结果、对象、时间筛选。 | | 前置条件 | 已执行客户创建、充值、权限拒绝、登录失败等操作。 | | 测试数据 | `module=customers`,`action=create`,`result=SUCCESS`,时间范围。 | | 步骤 | 1. 使用 `audit_viewer_001` 查询审计列表。2. 组合筛选条件。3. 打开审计详情。4. 无权限账号查询。 | | 预期结果 | 返回匹配审计;详情包含 actor、module、action、objectId、result、requestId、ip、userAgent、摘要。无权限返回 403。 | | 数据检查 | 分页正确;失败审计和成功审计均可查。 | | 安全检查 | 审计查询本身也应被审计或至少有访问日志。 | #### AUD-002 敏感字段脱敏 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证密码、token、secret、HA1、验证码等敏感字段在审计和日志中脱敏。 | | 前置条件 | 执行登录失败、重置密码、创建 SIP 网关、刷新 token 等操作。 | | 测试数据 | 明文密码 `SensitivePass-001`,SIP 密码 `SipSecret-001`。 | | 步骤 | 1. 执行敏感操作。2. 查询审计详情和应用日志。3. 全文搜索敏感明文。 | | 预期结果 | 敏感字段显示 `[REDACTED]` 或不出现;明文密码、refresh token、sipPassword、sipHa1 不可检索。 | | 数据检查 | 脱敏不影响非敏感字段排障。 | | 安全检查 | 嵌套对象和数组内敏感字段也脱敏。 | ### 8.8 前端页面与交互 本节覆盖 Web 登录、菜单、页面切换、错误/空状态、表单、权限按钮、构建产物和浏览器控制台。建议使用真实 B 环境或本地 mock API 两种方式执行。 公共前置数据: - 管理员账号:具备全部核心页面权限。 - 只读账号:仅具备 `dashboard.view`、`customers.view`。 - 各核心列表至少有 1 条数据,同时准备空数据环境或 mock 返回空数组。 - 浏览器打开 devtools,记录 console error/warn 和 network 失败。 #### WEB-001 登录态启动 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 Web 首屏、验证码、登录、refresh 和 Dashboard 初始化。 | | 前置条件 | API ready;Web 静态资源可访问。 | | 测试数据 | 管理员账号。 | | 步骤 | 1. 清空 cookie/localStorage。2. 打开首页。3. 完成验证码和登录。4. 刷新浏览器。 | | 预期结果 | 登录成功进入 Dashboard;刷新后通过 refresh 恢复登录态或合理回到登录页;无空白页;无 uncaught error。 | | 数据检查 | Network 中 refresh/login 状态符合预期。 | | 安全检查 | access token 不应出现在 URL;退出后不能继续访问受保护页面。 | #### WEB-002 API 失败态 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证 API 500、网络失败、401/403 时页面不崩溃。 | | 前置条件 | 可通过 mock 或临时停 API 触发错误。 | | 测试数据 | Dashboard、客户、话单、质检页面各触发一次失败。 | | 步骤 | 1. 打开页面。2. 让接口返回 500/超时/403。3. 点击重试或切换页面。 | | 预期结果 | 页面显示错误提示;布局保持;可重试;401 跳登录或刷新令牌;403 显示无权限。 | | 数据检查 | 不产生重复写请求。 | | 安全检查 | 错误提示不展示堆栈、SQL 或 token。 | #### WEB-003 空数据态 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证核心列表在空数据时可读、可继续创建或提示清晰。 | | 前置条件 | mock 返回空数组或测试库清空对应模块。 | | 测试数据 | 客户、供应商、网关、线路组、话单、质检、操作日志为空。 | | 步骤 | 1. 逐页打开空列表。2. 检查空状态和主操作。 | | 预期结果 | 显示空状态;有权限账号可见新增/导入按钮;只读账号不显示写按钮;无表格错位。 | | 数据检查 | 空列表不触发异常请求循环。 | | 安全检查 | 无权限不因空状态显示写入口。 | #### WEB-004 表单校验 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证前端表单必填、格式、后端错误显示和提交防抖。 | | 前置条件 | 管理员登录。 | | 测试数据 | 客户、客户网关、业务前缀、落地网关、线路组、充值/扣款、质检评分表单。 | | 步骤 | 1. 提交空表单。2. 输入非法金额/IP/时间/前缀。3. 提交合法数据。4. 快速重复点击提交。 | | 预期结果 | 非法输入有明确提示;后端错误展示在表单或通知中;合法提交关闭弹窗并刷新列表;重复点击不产生重复记录。 | | 数据检查 | 幂等接口使用 idempotencyKey;非幂等写操作避免双提交。 | | 安全检查 | 前端校验不能替代后端校验,绕过前端仍应被后端拒绝。 | #### WEB-005 页面导航回归 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证核心菜单逐页切换无空白、标题正确、权限裁剪有效。 | | 前置条件 | 管理员和只读账号可登录。 | | 测试数据 | 全部核心菜单:Dashboard、当前通话、客户、网关、业务前缀、充值、供应商、落地、线路组、号码库、话单、质检、用户、角色、操作日志。 | | 步骤 | 1. 管理员逐个点击菜单。2. 记录 h1/页面标题。3. 只读账号登录检查菜单。4. 浏览器刷新每个关键页面。 | | 预期结果 | 每页标题正确;无 runtime error;权限菜单和按钮符合权限矩阵;刷新不丢状态。 | | 数据检查 | 只请求当前权限允许的全局 API,避免批量 403。 | | 安全检查 | 手动输入无权限路由显示无权限或跳转。 | #### WEB-006 构建产物发布 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证前端 build 产物被正确发布到 B 当前 release,Nginx 首页引用新资源。 | | 前置条件 | 本地 build 成功;B 当前 release 明确;有回滚点。 | | 测试数据 | 新 JS/CSS hash。 | | 步骤 | 1. 执行 build。2. 发布 dist。3. `curl -k https://127.0.0.1/` 检查资源引用。4. 浏览器强刷首页。 | | 预期结果 | 首页引用新 JS/CSS;旧资源不被引用;Nginx 返回 200;页面可登录。 | | 数据检查 | release 目录和 public 目录一致。 | | 安全检查 | 发布不覆盖后端 env、node_modules 或用户上传录音。 | ### 8.9 性能、故障与安全 本节覆盖小规模并发、Worker/数据服务故障、Redis 热路径故障、SIP 安全探针、API 越权/重放和恢复性。执行故障类用例前必须确认回滚点和当前环境可中断。 #### PERF-001 5 路并发呼叫 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证最小并发下 SIP、CDR、计费、录音闭环稳定。 | | 前置条件 | A/B/T 服务 active;测试客户余额充足;T 脚本使用随机本地端口。 | | 测试数据 | 5 个唯一 Call-ID,持话 6-10 秒。 | | 步骤 | 1. T 并发发起 5 路呼叫。2. 等待全部挂断。3. 查询 CDR、rated、recordings。 | | 预期结果 | 5 路均 200 OK 和 BYE 200;生成 5 raw、5 rated、5 READY 录音;无端口冲突。 | | 数据检查 | 费用合计、余额扣减、录音大小合理。 | | 安全检查 | 并发不绕过 CPS/并发限制。 | #### PERF-002 12 路短突发 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证短突发下脚本、OpenSIPS、Worker 和录音搬运不丢事件。 | | 前置条件 | CPS/并发限制允许 12 路,或预期部分被限流。 | | 测试数据 | 12 个唯一 Call-ID,短持话。 | | 步骤 | 1. 同时发起 12 路。2. 记录成功/失败数量。3. 查询 CDR、录音、Redis pending/deadletter。 | | 预期结果 | 若限制允许则全部成功;若限流则失败数量和原因符合规则;无端口占用错误;pending/deadletter 正常。 | | 数据检查 | raw CDR 数量等于成功和可记录失败事件总数。 | | 安全检查 | 突发结束后正常呼叫恢复。 | #### FAIL-001 Recording Worker 停止 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 Recording Worker 停止期间通话不受影响,恢复后录音可补搬。 | | 前置条件 | 可停止 B `lisglosips@recording-worker`。 | | 步骤 | 1. 停止 Recording Worker。2. 发起成功呼叫。3. 确认录音仍在 A ready 目录。4. 恢复 Worker。5. 查询 recordings。 | | 预期结果 | 通话成功;恢复后录音变 READY;A 源文件校验后删除。 | | 数据检查 | 不重复生成录音记录。 | | 安全检查 | 停止 Worker 不影响 API 登录和 CDR 计费。 | #### FAIL-002 CDR Worker 停止 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 CDR Worker 停止期间 Redis Stream 积压,恢复后自动入库计费。 | | 前置条件 | 可停止 B `lisglosips@cdr-worker`。 | | 步骤 | 1. 停止 CDR Worker。2. 发起呼叫。3. 查看 Redis stream lag/pending。4. 恢复 Worker。5. 查询 raw/rated 和余额。 | | 预期结果 | 停止期间 SIP 通话成功但 MySQL 暂无 rated;恢复后 lag/pending 归零,CDR 入库,余额扣减一次。 | | 数据检查 | deadletter 无新增。 | | 安全检查 | 积压期间不丢 CDR。 | #### FAIL-003 MySQL 短故障 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 MySQL 短故障时 CDR 可重试,不误 ACK 或 deadletter。 | | 前置条件 | 已确认停止 MySQL 的影响和恢复命令;非生产或维护窗口。 | | 步骤 | 1. 停 MySQL。2. 发起呼叫。3. 查看 CDR Worker 日志、Redis pending/deadletter。4. 恢复 MySQL。5. 等待 pending 重试。 | | 预期结果 | 通话可成功;CDR 暂未入库但保留 pending;恢复后入库计费;deadletter 无新增。 | | 数据检查 | 余额只扣一次。 | | 安全检查 | 日志不泄露数据库密码。 | #### FAIL-004 Redis 短故障 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 Redis 热路径不可用时 A 保护性拒绝呼叫,恢复路径明确。 | | 前置条件 | 已确认停止 Redis 影响;A/B/T 可操作。 | | 步骤 | 1. 停 B Redis。2. 发起呼叫。3. 恢复 Redis。4. 按 Runbook 重试,如仍失败则重启 A OpenSIPS。5. 再发起呼叫。 | | 预期结果 | Redis 故障期间 A 返回保护性 503 或等价失败;恢复后按 Runbook 恢复;复测呼叫成功。 | | 数据检查 | 故障期间不产生错误扣费。 | | 安全检查 | Redis 恢复不使用过期错误配置。 | #### SEC-001 非法来源 SIP 探针 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证非白名单来源不能进入业务路由。 | | 前置条件 | 准备非客户网关 IP 主机。 | | 步骤 | 1. 从非白名单 IP 向 A `15060/udp` 发送 INVITE。2. 观察响应、A 日志、HOMER、CDR。 | | 预期结果 | 请求被静默丢弃或明确拒绝;不产生业务 CDR;不进入落地路由。 | | 数据检查 | 无客户识别、无余额变化。 | | 安全检查 | 不暴露可用客户 IP 或路由信息。 | #### SEC-002 超 CPS 探针 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证客户侧或落地侧 CPS 限制生效,超限后可恢复。 | | 前置条件 | 配置明确 CPS 限制,例如 2 CPS。 | | 步骤 | 1. 短时间发起超过限制的呼叫。2. 记录成功/失败数量。3. 等待窗口恢复后发起正常呼叫。 | | 预期结果 | 超限请求被限制;限流日志/指标可见;窗口后正常呼叫成功。 | | 数据检查 | 超限失败不扣费。 | | 安全检查 | 限流不影响其他客户。 | #### SEC-003 API 重放和越权 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证幂等接口重放安全、非幂等接口权限保护和审计。 | | 前置条件 | 准备只读账号、财务账号和管理员账号。 | | 步骤 | 1. 重放客户充值/扣款 idempotencyKey。2. 使用同 key 不同 body。3. 只读账号直接 POST 写接口。4. 伪造/过期 token 请求。 | | 预期结果 | 相同请求幂等返回同一结果;不同 body 返回 409;越权返回 403;未认证/伪造 token 返回 401。 | | 数据检查 | 余额不重复变化;失败审计可追踪。 | | 安全检查 | 错误响应不泄露 token 解析细节。 | #### SEC-004 录音与文件安全专项 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证录音播放、X-Accel 和 Worker 文件处理无路径穿越。 | | 前置条件 | READY 录音存在。 | | 步骤 | 1. 请求伪造录音 ID。2. 构造异常 storageKey。3. 检查 Nginx internal 路径。 | | 预期结果 | 非法路径返回 404/安全错误;无法读取录音根目录外文件。 | | 数据检查 | 无录音状态变化。 | | 安全检查 | 不泄露真实路径。 | ### 8.10 发布、备份与回滚 本节覆盖 release artifact、B release preflight、MySQL/Redis 备份、隔离恢复、灰度呼叫、脚本语法、应用回滚和阿里云迁移前复验。执行前必须确认当前 release、上一 release、备份目录和回滚命令。 #### OPS-000 Release artifact 生成与校验 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 release 产物由固定脚本生成,包含部署所需 API/Web/Worker/packages/Prisma/infra 文件、manifest 和 SHA256,避免手工拼包。 | | 前置条件 | Linux 构建环境或 B 兼容环境;本地代码已通过 lint/typecheck/test/build;`releaseId` 已冻结。 | | 步骤 | 1. 执行 `pnpm release:artifact -- --release-id `。2. 检查 `dist/releases/.tar.gz` 和 `.sha256`。3. 解包到临时目录检查 `RELEASE_MANIFEST.json`、`public/index.html`、API/Worker dist、`node_modules`、Prisma schema/migrations、argon2id wasm、infra 脚本。4. 执行 `sha256sum -c`。 | | 预期结果 | artifact 生成成功;manifest 记录 releaseId、git commit、git status、Node/pnpm 版本、required entries;SHA256 校验通过;正式发布包不得由 Windows 原生 node_modules 生成。 | | 数据检查 | 解包目录文件数量合理,关键路径完整;`.env`、私钥、明文密码、`.codex-private` 不进入 artifact。 | | 安全检查 | 脚本输出不回显数据库密码、Redis 密码、JWT secret、SSH 密钥。 | #### OPS-001 B preflight | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 B 当前 release 和关键服务满足发布/验收基线。 | | 前置条件 | B 已部署 release;SSH 可达。 | | 步骤 | 1. 执行 `lisglosips-release-preflight.sh`。2. 检查 systemd 服务。3. 请求 API ready 和 HTTPS 首页。 | | 预期结果 | MySQL、Redis、Nginx、API、CDR Worker、Recording Worker、Config Publisher、HEP、Prometheus、Grafana 均 active;API ready 返回 ok;Nginx 配置测试通过。 | | 数据检查 | current symlink 指向预期 release;最近备份目录可见。 | | 安全检查 | 输出不包含密码、token、私钥。 | #### OPS-002 MySQL 备份校验 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 MySQL 备份完整、可校验、元数据可追踪。 | | 前置条件 | B MySQL 正常,有业务数据。 | | 步骤 | 1. 触发 MySQL 备份。2. 检查备份目录、`metadata.tsv`、`SHA256SUMS`。3. 执行 `sha256sum -c`。 | | 预期结果 | 全库和业务库备份文件存在;SHA256 校验通过;metadata 记录时间、主机、数据库、大小。 | | 数据检查 | 备份包含关键表:customers、raw_cdrs、rated_cdrs、recordings、users、recharges。 | | 安全检查 | 备份文件权限限制,避免普通用户读取敏感数据。 | #### OPS-003 Redis 备份校验 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证 Redis RDB 备份完整并可追踪配置版本和 Stream 数据。 | | 前置条件 | B Redis 正常,有 `cfg:active_version` 和 CDR Stream。 | | 步骤 | 1. 触发 Redis 备份。2. 检查 `dump.rdb`、metadata、SHA256。3. 记录 DBSIZE、active version、stream 长度。 | | 预期结果 | RDB 和 metadata 存在;SHA256 校验通过;关键 key 元数据记录完整。 | | 数据检查 | 包含配置版本、CDR Stream、幂等锁或按备份策略记录。 | | 安全检查 | Redis 备份权限受控。 | #### OPS-004 隔离恢复演练 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证备份能恢复到隔离环境,且不覆盖生产数据。 | | 前置条件 | 已有 MySQL 和 Redis 备份。 | | 步骤 | 1. MySQL 恢复到临时库。2. Redis RDB 启动到临时端口或 Unix socket。3. 核对关键表和 key 数量。4. 删除临时库/实例。 | | 预期结果 | 恢复成功;关键计数与备份时 metadata 匹配;演练不影响运行中 MySQL/Redis。 | | 数据检查 | raw_cdrs、recordings、recharges、users、Redis cfg 和 stream 数量合理。 | | 安全检查 | 临时恢复数据清理干净,权限不扩大。 | #### OPS-005 灰度呼叫验收 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证新 release 切换后核心 SIP-CDR-计费-录音闭环可用。 | | 前置条件 | B 已切到新 release;A 配置和 Redis active version 正常;T 可发起呼叫。 | | 步骤 | 1. T 发起一通灰度呼叫。2. 记录 Call-ID。3. 查询 SIP 响应、raw/rated CDR、余额扣减、录音 READY、HOMER。 | | 预期结果 | 呼叫成功;CDR 计费正确;录音 READY;HOMER 可查;无新增错误日志。 | | 数据检查 | 灰度数据 ID 记录到验收报告。 | | 安全检查 | 灰度只使用测试客户,不影响真实客户。 | #### OPS-006 回滚脚本语法 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证回滚脚本和 A 配置恢复脚本语法正确。 | | 前置条件 | 脚本已部署。 | | 步骤 | 1. 对 B release rollback/preflight 脚本执行 `bash -n`。2. 对 A OpenSIPS config restore 脚本执行 `bash -n`。 | | 预期结果 | 所有脚本语法通过。 | | 数据检查 | 语法检查不执行真实回滚。 | | 安全检查 | 不输出敏感参数。 | #### OPS-007 应用回滚演练 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证可将 B current 切回上一 release 并恢复服务。 | | 前置条件 | 有上一可用 release;当前无不可逆数据库迁移或已有数据回滚方案。 | | 步骤 | 1. 记录当前 release。2. 切换 current symlink 到上一 release。3. 重启 API/Worker。4. 执行 preflight、API ready、HTTPS 首页。5. 如演练要求,再切回新 release。 | | 预期结果 | 回滚后服务 active;API ready ok;Web 可访问;核心接口兼容;可再切回。 | | 数据检查 | 无数据库 schema 不兼容;Worker 不重复处理已完成消息。 | | 安全检查 | 回滚不覆盖备份、录音和用户上传数据。 | #### OPS-008 OpenSIPS 配置恢复演练 | 字段 | 内容 | | --- | --- | | 优先级 | P1 | | 目的 | 验证 A 侧 OpenSIPS 配置可语法检查和按 Runbook 恢复。 | | 前置条件 | 有当前运行配置备份;维护窗口或测试环境。 | | 步骤 | 1. 执行 `opensips -C` 检查当前配置。2. 对恢复脚本做 dry-run 或语法检查。3. 在测试窗口执行恢复并重启/ reload。4. 发起呼叫验证。 | | 预期结果 | 配置语法通过;恢复后 OpenSIPS active;呼叫成功。 | | 数据检查 | 配置 SHA256 与预期记录一致。 | | 安全检查 | 不把模板占位符配置直接覆盖生产运行配置。 | #### OPS-009 阿里云迁移前复验 | 字段 | 内容 | | --- | --- | | 优先级 | P0 | | 目的 | 验证本地 KVM 结论在阿里云网络、安全组、数据盘、TLS 和正式域名下重新成立。 | | 前置条件 | 阿里云 A/B 已部署,VPC、安全组、数据盘、DNS/TLS 准备完成。 | | 步骤 | 1. 复查安全组端口:SIP、RTP、HTTPS、管理端口。2. 执行 B preflight。3. 执行备份恢复演练。4. 执行灰度呼叫。5. 执行故障注入最小集。 | | 预期结果 | 正式环境全部通过;TLS 正常;RTP 双向媒体正常;备份恢复可用;灰度呼叫闭环。 | | 数据检查 | 迁移报告记录公网/私网 IP、端口、证书、备份路径和灰度 Call-ID。 | | 安全检查 | SSH 不对全网开放;Redis/MySQL/Prometheus/Grafana 不公网暴露;开发 CA 和本地路径不进入生产。 | ## 9. 缺陷分级 | 级别 | 定义 | 示例 | | --- | --- | --- | | P0 | 阻断核心链路或存在严重安全/数据风险 | 无法登录、呼叫失败、重复扣费、录音越权、发布后 API 不可用 | | P1 | 核心功能受影响但有规避方案 | 某管理页面写操作失败、Worker 恢复需人工处理、关键指标错误 | | P2 | 一般功能缺陷或体验问题 | 筛选条件不完整、提示文案不清晰、非关键统计延迟 | | P3 | 低风险优化 | 样式细节、日志可读性、非核心字段排序 | ## 10. 回归重点 每次涉及下列模块时必须扩大回归: - `apps/api/src/modules/auth`、`security`、`audit`:回归 AUTH、AUD、WEB 权限相关用例。 - `customer-gateways`、`business-prefixes`、`worker-config-publisher`:回归 GW、CALL 业务前缀和配置发布。 - `worker-cdr`、`packages/redis`:回归 CDR、充值幂等、FAIL-002/FAIL-003。 - `worker-recording`、`recordings`、`quality`:回归 REC、QA 和录音播放权限。 - `infra/server-a` OpenSIPS/RTPEngine:回归 CALL、PERF、SEC、HEP/HOMER。 - `infra/server-b` Nginx/systemd/release:回归 OPS、WEB-006、健康检查。 - `apps/web/src/App.jsx`、`pages`、`permissions.js`:回归 WEB-001 至 WEB-005、AUTH-007、AUTH-008。 ## 11. 当前已知风险与补充建议 - Redis 恢复后 OpenSIPS 热路径曾出现需要重启 `opensips` 才恢复的问题。上线前应补充 Redis 断连重连专项测试,并增加明确指标/告警。 - 当前真实 80 万手机号段尚未导入。号码库性能、导入耗时、批量校验和 CDR 归属地准确性需单独做数据量级测试。 - 真实 B 浏览器登录态下的逐页按钮、真实录音播放和 Nginx X-Accel 链路仍需人工复核。 - 阿里云迁移前必须重跑网络安全组、正式 TLS、数据盘、备份恢复、故障注入和灰度呼叫,不应直接沿用本地 KVM 结论。 - 建议后续增加 Playwright 前端冒烟:登录、菜单遍历、权限账号、关键表单、录音播放入口,降低页面空白类问题复发概率。