Files
lisglosips/docs/TEST_PLAN_AND_CASES.md
T

136 KiB
Raw Blame History

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、客户、客户网关、业务前缀、充值、供应商、落地网关、落地线路组、号码库、当前通话、话单、质检、用户、角色权限、操作日志。
  • APIapps/api/src/modules 下所有已实现模块。
  • WorkerCDR/Billing Worker、Recording Worker、Config Publisher。
  • 数据层:Prisma/MySQL schema、Redis 热路径、Redis Stream、Outbox、幂等键。
  • 通信层:Server A OpenSIPS、RTPEngine、HEPServer T SIP 模拟脚本。
  • 运维层:Nginx、TLS、systemd 服务、备份恢复、发布 preflight、回滚脚本、监控告警基础设施。

2.2 不纳入范围

  • 多 OpenSIPS 节点高可用。
  • Kubernetes、跨地域容灾、复杂账单/发票/对账。
  • 自研 SIP Trace UI、完整自研监控告警页面、完整系统设置页面。
  • 超长时间满载压测。当前以功能闭环、小规模并发、故障恢复和上线前冒烟为主。

3. 测试环境

环境 用途 说明
本地开发 单元、服务、API e2e、前端构建 Windows 工作区,Node.js 22+pnpm 10.33.0Vitest
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 lintpnpm typecheckpnpm 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 lintpnpm typecheckpnpm testpnpm build 通过。
  • API /api/v2/health/live/api/v2/health/ready 正常。
  • 新 release 必须由 pnpm release:artifact -- --release-id <release-id> 或等价 Linux 环境命令生成,并校验 RELEASE_MANIFEST.json.sha256
  • B release preflight 通过,Nginx 配置测试通过,关键 systemd 服务 active。
  • 至少完成一通灰度呼叫,并核对 SIP 200 OK、CDR 入库、计费、录音 READY。
  • 备份目录、SHA256 校验和回滚脚本已确认。
  • 无 P0/P1 缺陷;P2 缺陷有明确规避方案和负责人。

6. 自动化测试命令

在仓库根目录执行:

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

常用定向测试:

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.viewcustomers.viewcustomers.manageusers.viewusers.manageroles.viewquality.viewquality.managecdr.viewrecordings.play
  • 只读用户 viewer_auth_001:状态 ENABLED,仅包含 dashboard.viewcustomers.view
  • 质检用户 quality_auth_001:状态 ENABLED,包含 quality.viewquality.manage,不包含 users.viewusers.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. 使用正确用户名、密码、captchaIdcaptchaCode 请求 POST /api/v2/auth/login。3. 解析响应体和 Set-Cookie。4. 使用返回的 access token 请求一个管理员可访问接口,例如 GET /api/v2/users
预期结果 验证码接口返回 captchaIdimageDataUrlexpiresAt,不返回验证码答案;登录返回 200;响应体包含 accessTokenuseruser.rolesuser.permissions 非空且包含管理员权限;Set-Cookie 包含 refresh cookie。
数据检查 auth_sessions 或等价会话存储新增一条未撤销会话;库中只保存 refresh token hash,不保存明文 refresh token;用户失败次数清零。
安全检查 响应体不包含 passwordHashsipHa1、refresh token 明文、权限表以外的敏感字段;access token 为 Bearer token,不能通过 cookie 自动作为 access token 使用。

补充负向用例:

子 ID 场景 步骤 期望结果
AUTH-001-N1 缺少验证码 不传 captchaIdcaptchaCode 登录 返回 401,错误码 AUTH_INVALID_CREDENTIALSAUTH_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=5AUTH_LOCK_WINDOW_SECONDS=900AUTH_LOGIN_THROTTLE_MAX_FAILURES=10AUTH_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-CookiecookieB 为首次 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 返回 401cookieB 可以继续 refresh 并再次轮换。
数据检查 旧 session revokedAt 被设置;新 session 未撤销;refresh token hash 发生变化;不会出现多个同一 refresh hash 的活跃会话。
安全检查 refresh 接口不接受 Bearer token 替代 cookie;响应体不返回 refresh token 明文;cookie 设置 HttpOnlySameSite、合理 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/usersGET /api/v2/customersGET /api/v2/recordings;公开接口:GET /api/v2/auth/captchaPOST /api/v2/auth/loginPOST /api/v2/auth/refreshGET /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.viewcustomers.viewquality_auth_001 已登录,仅有质检权限。
测试数据 viewer access tokenquality 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.viewcustomers.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.viewcustomers.managerecharges.viewrecharges.manageaudit.view
  • 客户只读账号 customer_viewer_001:仅包含 customers.view
  • 财务账号 finance_recharge_001:包含 recharges.viewrecharges.managecustomers.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 CUS001contactName=Opsphone=13800000001email=ops-cus001@example.testdomain=cus001.example.testbillingMode=POSTPAIDcreditLimit=88.5minBalance=2.25notes=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;客户状态为 ENABLEDbillingMode=POSTPAIDbalance=0.000000 或系统默认初始余额;creditLimit=88.500000minBalance=2.250000;列表可见该客户。
数据检查 customers.created_by 为当前用户;version 初始化正确;deleted_at 为空;审计存在 module=customersaction=createresult=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
测试数据 更新 contactNamephoneemailnotes;状态切换 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_byupdated_atversion 按更新变化;启停分别写入 customers:disablecustomers:enable 审计;余额只能通过充值或计费链路变化。
安全检查 customers.manage 的用户更新、启停均返回 403;禁用客户不应影响其他客户。

补充负向用例:

子 ID 场景 步骤 期望结果
CUS-003-N1 更新不存在客户 PATCH /customers/not-found 返回 404 或明确业务错误
CUS-003-N2 无效金额字段 creditLimit=-1minBalance=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.25idempotencyKey=cus005-20260628-0001remark=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. 查询审计。
预期结果 充值返回 201amount=15.250000beforeBalance=10.000000afterBalance=25.250000;客户余额更新为 25.250000;充值列表包含该流水,状态 SUCCEEDED
数据检查 customer_recharges 或统一充值/扣款流水表新增一条;idempotency_keys 新增成功记录;余额和流水前后值一致;created_by 为财务用户。
安全检查 只读用户请求充值返回 403;充值/扣款接口不接受前端传入 beforeBalanceafterBalance 覆盖服务端计算。

补充负向用例:

子 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.25idempotencyKey=cus006-deduct-0001remark=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. 查询审计。
预期结果 扣款返回 201amount=-3.250000beforeBalance 为扣款前余额;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-0001cus007-replay-deduct-0001 未被使用。
测试数据 入金:amount=0.000001idempotencyKey=cus007-replay-0001;扣款:amount=-0.000001idempotencyKey=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-0001cus_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
目的 验证客户授信/最低余额和充值/扣款金额的格式、范围和归一化,避免浮点误差和非法金额进入财务数据。
前置条件 有可创建/更新客户权限和充值权限。
测试数据 客户金额合法值:00.00000111.299999999999999.999999;充值/扣款合法值:0.000001-0.0000011.2-1.299999999999999.999999-99999999999999.999999;非法金额:客户金额字段中的负数、充值/扣款中的 0-00.000000-0.0000001.1234567-1.1234567100000000000000-100000000000000abc、空字符串、科学计数法 1e3
步骤 1. 创建/更新客户时分别提交合法和非法 creditLimitminBalance。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=CUSTOMERaccountId=cus_balance_seed_001take=10skip=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 查询。
预期结果 客户列表返回余额、授信、最低余额、可用余额、网关数量;客户详情字段完整;充值/扣款列表按筛选返回,包含 itemstotal;正数金额和负数金额均原样展示为 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.viewcustomer_gateways.manageaudit.view
  • 只读账号 gateway_viewer_001:仅包含 customer_gateways.view
  • 客户 cus_gateway_seed_001:状态 ENABLED,余额充足,未删除。
  • 线路组 llg_gateway_seed_001:状态 ENABLED,至少包含 1 个可用落地网关。
  • 业务前缀 bp_671_seedprefix=671,状态 ENABLED,优先级 10
  • 客户网关 cgw_ip_seed_001authMode=IPsourceIps=["100.93.185.30"],绑定 llg_gateway_seed_001
  • Redis 配置发布测试环境可读取 cfg:active_versioncfg:v:{version}:* key。

GW-001 创建 IP 认证客户网关

字段 内容
优先级 P0
目的 验证 IP 鉴权客户网关创建、源 IP 校验、多 IP 去重、计费参数归一化、审计和配置发布 outbox。
前置条件 gateway_ops_001 已登录;客户和线路组存在;待创建网关名 Auto IP Gateway GW001 不重复。
测试数据 customerId=cus_gateway_seed_001name=Auto IP Gateway GW001authMode=IPsourceIps=["100.93.185.30","100.93.185.31","100.93.185.30"]lineGroupId=llg_gateway_seed_001billingCycleSec=6cycleRate=0.01callerMatchMode=ANYcalleeMatchMode=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 和审计。
预期结果 创建返回 201authMode=IPsourceIps 去重后保留两个 IPsourceIp 兼容字段为第一个 IPsipUsernamesipDomain 为空;billingCycleSec=6cycleRate=0.010000;状态默认 ENABLED
数据检查 写入客户网关主表和网关 IP 明细;生成 aggregateType=customer_gateway_config 的待发布 outbox;审计存在 module=customer_gatewaysaction=createresult=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.1abc 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=086401 400 INTEGER_INVALID
GW-001-N6 费率非法 cycleRate=-11.1234567 400 MONEY_INVALID

GW-002 创建 SIP Digest 网关

字段 内容
优先级 P0
目的 验证 SIP Digest 鉴权网关创建、HA1 生成、域名归一化、密码不回显和不进入审计。
前置条件 gateway_ops_001 已登录;客户和线路组存在;SIP 用户名未重复。
测试数据 authMode=SIP_DIGESTsipUsername=alice-gwsipDomain=SIP.EXAMPLE.LOCALsipPassword=change-me-very-stronglineGroupId=llg_gateway_seed_001calleeMatchMode=BUSINESS_PREFIXESbusinessPrefixIds=["bp_671_seed"]
步骤 1. 请求 POST /api/v2/customer-gateways 创建 SIP 网关。2. 请求列表和详情。3. 查询数据库中 sip_ha1。4. 查询审计日志。
预期结果 创建返回 201sipDomain 归一化为小写;hasSipCredential=true;列表和详情不返回 sipPasswordsipHa1sourceIps=[];业务前缀绑定可见。
数据检查 数据库存储 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=MIXEDsourceIps=["100.93.185.40"]sipUsername=mixed-gwsipDomain=sip.example.localsipPassword=change-me-very-strong
步骤 1. 创建 MIXED 网关。2. 查询详情。3. 发布配置后检查 Redis IP 和 SIP 索引。
预期结果 创建成功;响应同时包含 sourceIpssipUsernamesipDomainhasSipCredential=trueRedis 写入 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=PREFIXEScallerPrefixes=["021","010","+8610","021"];另准备同 IP 其他网关已有主叫前缀 0219021
步骤 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 按优先级/前缀保存;生成 outboxmanifest 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_seedbp_672_seed 均启用;客户网关启用;A 侧 OpenSIPS 加载业务前缀热路径。
测试数据 calleeMatchMode=BUSINESS_PREFIXESbusinessPrefixIds=["bp_671_seed","bp_672_seed"];测试被叫 67113800138000
步骤 1. 更新客户网关绑定业务前缀。2. 发布配置。3. 检查 Redis business_prefix_value:671customer_gateway:{id}:business_prefixes。4. T 发起被叫带业务前缀的呼叫。5. 同时准备一个空业务前缀/主叫前缀网关作为兜底,再次发起 671... 呼叫。6. 查询 CDR。
预期结果 网关详情展示绑定前缀;发布 manifest businessPrefixCountcustomerGatewayBusinessPrefixCount 正确;呼叫按 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_seedB 为 calleeMatchMode=ANYcallerPrefixes=["021"]C 为 calleeMatchMode=ANYcallerMatchMode=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=673name=业务673description=for GW-007priority=30status=ENABLED
步骤 1. 请求 POST /api/v2/business-prefixes 创建。2. 请求 GET /api/v2/business-prefixes?status=ENABLED&keyword=673。3. 更新 prioritydescription。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_ERRORBUSINESS_PREFIX_INVALID
GW-007-N3 优先级非法 priority=010000、小数 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
测试数据 策略 1callerMode=PREFIXcallerValue=021calleeMode=ANY;策略 2callerMode=ANYcalleeMode=EQUALScalleeValue=13800138000fallback 策略: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_createpolicy_updatepolicy_reorderpolicy_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=010001、字符串 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_configcustomer_gateway_config outboxRedis 可用;MySQL 可读。
测试数据 启用客户、业务前缀 671、IP 网关、多 IP、主叫前缀、业务前缀绑定、至少 1 条策略、线路组。
步骤 1. 执行 Config Publisher。2. 读取 cfg:active_versioncfg:previous_version。3. 读取 cfg:manifest:{version} 或等价 manifest key。4. 检查 Redis 热路径 key。5. 检查 outbox 状态。
预期结果 Publisher 返回 published=trueactive version 更新;manifest schemaVersion=2 且 counts 正确;outbox 由 PENDING/PROCESSING 变为 PUBLISHEDlastError=null
Redis 检查 存在 cfg:v:{version}:auth:ip:{sourceIp}auth:ip:{sourceIp}:gatewaysauth:sip:{username}@{domain}business_prefix_value:671business_prefixescustomer_gateway:{id}customer_gateway:{id}:caller_prefixescustomer_gateway:{id}:business_prefixescustomer_gateway:{id}:policiescustomer_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_001gateway_viewer_001 均可登录;后端至少存在 1 条业务前缀。
测试数据 prefix=674name=业务674priority=40
步骤 1. 使用 gateway_viewer_001 登录并进入业务前缀页面。2. 检查列表、筛选、空/错状态。3. 确认 viewer 无新增/编辑/启停/删除按钮。4. 使用 gateway_ops_001 登录,创建、编辑、禁用、启用一条测试前缀。5. 打开浏览器 console。
预期结果 页面正常渲染,无空白;viewer 只读;ops 可完成 CRUD;表单默认值正确;接口错误时显示错误提示;console 无 uncaught error。
数据检查 页面操作产生对应后端记录、outbox 和审计;删除测试前缀后列表状态正确。
安全检查 前端隐藏按钮之外,直接调用无权限写接口仍返回 403。

字段校验矩阵:

对象 字段 合法规则 非法示例 期望错误
客户网关 authMode IPSIP_DIGESTMIXED PASSWORD AUTH_MODE_INVALID
客户网关 sourceIps IP/MIXED 必填,最多 20 个 IPv4/IPv6 abc、超过 20 个 SOURCE_IP_REQUIREDSOURCE_IP_INVALIDSOURCE_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_WEAKSIP_PASSWORD_REQUIRED
客户网关 billingCycleSec 1-86400 整数 0864011.5 INTEGER_INVALID
客户网关 cycleRate 非负,最多 14 位整数、6 位小数 -11.1234567 MONEY_INVALID
客户网关 callerPrefixes PREFIXES 模式必填,最多 20 个,安全字符 空、含空格、超过 20 个 CALLER_PREFIX_REQUIREDCALLER_PREFIX_INVALIDLIST_TOO_LARGE
客户网关 businessPrefixIds BUSINESS_PREFIXES 模式必填,最多 50 个 空、超过 50 个 BUSINESS_PREFIX_REQUIREDLIST_TOO_LARGE
客户网关 匹配唯一性 同一 IP + 同一业务前缀唯一;同一 IP 主叫前缀不互相覆盖;空业务前缀兜底唯一 重复 bp_671_seed0210219、两个 ANY 被叫兜底 CUSTOMER_GATEWAY_MATCH_CONFLICTCUSTOMER_GATEWAY_CALLER_PREFIX_OVERLAP
业务前缀 prefix 1-32 位字母或数字,唯一 67#、中文、重复 BUSINESS_PREFIX_INVALID 或 409
业务前缀 priority 1-9999 整数 0100001.5 PRIORITY_INVALID
策略 callerMode/calleeMode ANYEQUALSPREFIX REGEX MATCH_MODE_INVALID
策略 callerValue/calleeValue 非 ANY 时必填,最多 64 位安全字符 空、138 001 VALIDATION_ERRORMATCH_VALUE_INVALID

8.4 供应商、落地网关与线路组

本节覆盖供应商主数据、供应商充值、落地网关出局参数、禁呼时段、编码、主被叫改写、主叫号码池、线路组成员、优先级、并发汇总、配置发布和真实呼叫选路。后端接口统一以 /api/v2 为前缀。

公共前置数据:

  • 供应商管理账号 vendor_ops_001:包含 vendors.viewvendors.managevendor_gateways.viewvendor_gateways.manageline_groups.viewline_groups.managerecharges.viewrecharges.manageaudit.view
  • 只读账号 vendor_viewer_001:包含 vendors.viewvendor_gateways.viewline_groups.view
  • 供应商 ven_seed_001:余额 20.000000,授信 200.000000。供应商不提供启用/禁用状态入口。
  • 落地网关 vgw_primary_001:状态 ENABLEDhost 指向 T/UAS 或可控落地模拟端。
  • 落地网关 vgw_backup_001:状态 ENABLED,用于线路组故障转移。
  • 线路组 llg_vendor_seed_001:状态 ENABLED,至少包含 vgw_primary_001vgw_backup_001
  • 客户网关策略已指向 llg_vendor_seed_001,用于真实呼叫选路验证。

VEN-001 创建供应商

字段 内容
优先级 P0
目的 验证供应商主数据创建、默认余额/授信、可用余额、审计和权限。
前置条件 vendor_ops_001 已登录;供应商名 Auto Vendor VEN001 不存在。
测试数据 name=Auto Vendor VEN001contactName=NOCphone=4001000001email=noc-ven001@example.testcreditLimit=188.5settlement=monthlynotes=created by VEN-001
步骤 1. 请求 POST /api/v2/vendors。2. 使用返回 ID 请求 GET /api/v2/vendors/:id。3. 请求 GET /api/v2/vendors。4. 查询审计。
预期结果 创建返回 201;余额为系统默认值;creditLimit=188.500000availableBalance=balance+creditLimit;列表可见,gatewayCount=0;响应不暴露供应商启停状态。
数据检查 写入 created_byupdated_atdeleted_at=null;审计存在 module=vendorsaction=createresult=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=DISABLEDstatus=DELETED 不允许通过供应商接口维护启停;响应不返回供应商状态,且不会生成启停审计
VEN-001-N4 授信金额非法 creditLimit=-11.1234567 400 MONEY_INVALID
VEN-001-N5 重复名称 创建已存在供应商名称 409 或明确业务错误,不重复写入

VEN-002 供应商更新与删除保护

字段 内容
优先级 P1
目的 验证供应商资料更新、软删除,以及有关联落地网关时的删除保护。
前置条件 已存在供应商 ven_seed_001;另有无关联供应商 ven_delete_free_001
测试数据 更新 notes=primary carriercreditLimit=200
步骤 1. PATCH 更新供应商资料。2. 请求旧的 /vendors/:id/disable/vendors/:id/enable。3. 删除有关联落地网关的供应商。4. 删除无关联供应商。
预期结果 更新成功且金额归一化;供应商启停接口不可用;有关联供应商删除被拒绝或按产品规则受控软删除;无关联供应商可软删除。
数据检查 updated_byversion 或更新时间更新;删除写审计;不会产生供应商 enable/disable 审计;删除不物理清除历史充值和网关引用。
安全检查 vendors.manage 的更新、删除返回 403;旧启停接口不应暴露可操作功能。

VEN-003 供应商充值

字段 内容
优先级 P0
目的 验证供应商人工充值会增加供应商余额、生成充值流水、幂等记录和审计。
前置条件 vendor_ops_001 已登录;供应商 ven_seed_001 当前余额已知。
测试数据 amount=3.5idempotencyKey=ven003-20260628-0001remark=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. 重放相同请求。
预期结果 第一次返回 201amount=3.500000afterBalance=beforeBalance+3.500000;供应商详情余额同步增加;重放返回同一流水 ID,不重复加钱。
数据检查 充值流水 accountType=VENDOR;幂等 key 成功记录唯一;审计存在 module=rechargesaction=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_001name=Auto IP Vendor GatewayauthMode=IPhost=203.0.113.10port=5060transport=udpcpsLimit=30concurrencyLimit=300billingCycleSec=6cycleRate=0.012landingCalleePrefix=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_gatewaysaction=create;供应商 gatewayCount 增加。
安全检查 vendor_viewer_001 创建返回 403;响应不包含 SIP 密钥或内部错误。

VGW-002 创建 SIP/MIXED 落地网关

字段 内容
优先级 P1
目的 验证落地侧 SIP Digest/MIXED 鉴权、HA1 生成、密码不回显和身份变更规则。
前置条件 供应商存在;SIP 用户名未重复。
测试数据 authMode=SIP_DIGESThost=SIP.CARRIER.LOCALsipUsername=carrier-usersipPassword=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=0128 400 INTEGER_INVALID
VGW-003-N3 时间格式非法 startTime=24:00:0023:00 400 TIME_INVALID
VGW-003-N4 跨午夜时段 startTime=23:00:00endTime=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=86prefixRules=[{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 被叫符合 landingCalleePrefixCALLEE 改写规则;主叫符合 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_001vgw_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;成员 AvendorGatewayId=vgw_primary_001priority=1weight=2concurrencyCap=60;成员 BvendorGatewayId=vgw_backup_001priority=2weight=1concurrencyCap=40
步骤 1. 请求 POST /api/v2/landing-line-groups。2. 添加两个成员。3. 请求详情和列表。4. 发布配置并检查 Redis。
预期结果 线路组创建成功;itemCount=2enabledItemCount=2concurrencyCapSum=100gatewayConcurrencyLimitSum 等于启用成员网关并发限制合计;Redis 写入 line_group:{id}line_group:{id}:items
数据检查 每次写操作生成 line_group_config outbox;审计包含 createitem_create
安全检查 只读用户可查看不可写,写返回 403。

LLG-002 线路组成员更新与重排

字段 内容
优先级 P0
目的 验证线路组成员优先级、权重、并发上限和状态影响路由顺序。
前置条件 线路组 llg_vendor_seed_001 含 primary 和 backup 两个成员。
测试数据 将 backup 重排到第一位;将 primary status=DISABLED;调整 concurrencyCap=20weight=5
步骤 1. 请求 POST /api/v2/landing-line-groups/:id/items/reorder。2. PATCH 更新成员状态/权重/并发。3. 发布配置。4. 发起呼叫确认选路。
预期结果 详情中成员顺序按新 priority;禁用成员不计入 enabledItemCount 和并发汇总;呼叫按新顺序和状态选路。
数据检查 Redis line_group:{id}:items 顺序正确;生成 item_updateitem_reorder 审计和 outbox。
安全检查 重排必须使用同一线路组完整成员集合或按产品规则拒绝。

补充负向用例:

子 ID 场景 步骤 期望结果
LLG-002-N1 权重非法 weight=01001 400 INTEGER_INVALID
LLG-002-N2 并发上限非法 concurrencyCap=-1100001 400 INTEGER_INVALID
LLG-002-N3 priority 非法 priority=010001 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_001primary 和 backup 均可被 T/UAS 验证。
测试数据 一通正常呼叫;一组 primary 禁用或禁呼时段内呼叫;一组并发接近上限呼叫。
步骤 1. 发布当前配置。2. 发起正常呼叫,确认落到 primary。3. 禁用 primary 或设置当前禁呼,发布后再呼叫。4. 配置 primary 并发上限较低,发起多路并发。5. 查询 CDR、录音和落地网关成本。
预期结果 正常呼叫选 primaryprimary 不可用时转 backup;并发达到上限后新呼叫转备用或被拒绝;CDR 记录实际落地网关、供应商成本、失败原因。
数据检查 raw_cdrs/rated_cdrs 中 vendorGatewayId、lineGroupId、cost 字段正确;HOMER 可看到出局 INVITE 目标。
安全检查 故障转移不绕过客户鉴权、余额检查、业务前缀和主叫限制。

字段校验矩阵:

对象 字段 合法规则 非法示例 期望错误
供应商 name 必填,1-120 字符,唯一 空、超长、重复 VALIDATION_ERROR 或 409
供应商 creditLimit 非负,最多 14 位整数、6 位小数 -11.1234567 MONEY_INVALID
落地网关 authMode IPSIP_DIGESTMIXED PASSWORD AUTH_MODE_INVALID
落地网关 host IP 或小写 hostname 规则 bad_host! HOST_INVALID
落地网关 port 1-65535 整数 065536 INTEGER_INVALID
落地网关 transport udptcptls ws TRANSPORT_INVALID
落地网关 sipUsername SIP/MIXED 必填,安全字符 carrier user SIP_USERNAME_INVALID
落地网关 sipPassword SIP/MIXED 创建或身份变更必填,至少 12 字符 short SIP_PASSWORD_WEAKSIP_PASSWORD_REQUIRED
落地网关 cpsLimit 1-10000 整数,只允许数字 01000120 CPS INTEGER_INVALID
落地网关 concurrencyLimit 0-100000 整数,0 表示不限 -1100001 INTEGER_INVALID
落地网关 billingCycleSec 1-60 整数 061 INTEGER_INVALID
落地网关 cycleRate 非负,最多 14 位整数、6 位小数 -11.1234567 MONEY_INVALID
落地网关 landingCalleePrefix 可空,字母数字 86+ LANDING_CALLEE_PREFIX_INVALID
落地网关 forbiddenPeriods 数组,weekdayMask 1-127,时间 HH:mm:ss 非数组、24:00:00 FORBIDDEN_PERIODS_INVALIDTIME_INVALID
落地网关 codecs 数组,codec/priority 唯一 重复 codec 或 priority CODEC_DUPLICATECODEC_PRIORITY_DUPLICATE
落地网关 prefixRules 数组,direction 为 CALLER/CALLEE,同方向 priority 唯一 BOTH、重复优先级 PREFIX_DIRECTION_INVALIDPREFIX_RULE_PRIORITY_DUPLICATE
落地网关 callerRewritePool 最多 50 条,caller 唯一,weight 1-100000 重复 caller、weight=0 CALLER_REWRITE_DUPLICATEINTEGER_INVALID
线路组 name 必填,1-120 字符 空、超长 VALIDATION_ERROR
线路组成员 priority 1-10000 整数 010001 INTEGER_INVALID
线路组成员 weight 1-1000 整数 01001 INTEGER_INVALID
线路组成员 concurrencyCap 0-100000 整数,0 表示不限 -1100001 INTEGER_INVALID

8.5 SIP 呼叫、CDR 与计费

本节覆盖客户接入、SIP 注册/鉴权、业务前缀识别、线路组出局、Redis Stream CDR、CDR Worker 计费、客户余额扣减、失败话单、幂等、号码归属地和话单查询。三机端到端用例默认在 A/B/T 联调环境执行。

公共前置数据:

  • 客户 cus_call_seed_001:状态 ENABLEDbillingMode=PREPAID,余额充足,授信和最低余额按测试场景设置。
  • IP 客户网关 cgw_call_ip_001authMode=IPT 源 IP 已在 sourceIps,启用,绑定业务前缀 671 和线路组 llg_call_seed_001
  • SIP 客户网关 cgw_call_sip_001authMode=SIP_DIGEST,账号密码已知,启用,绑定线路组。
  • 线路组 llg_call_seed_001:启用,包含 vgw_call_primary_001vgw_call_backup_001
  • 落地网关 vgw_call_primary_001:启用,计费周期 6 秒,费率 0.012000T/UAS 可接收。
  • 业务前缀 bp_671_seedprefix=671,启用。
  • 号码库包含 1380013 号段,归属地和运营商字段可预测。
  • Config Publisher 已成功发布,A 侧 OpenSIPS 使用最新 cfg:active_version

CALL-001 IP 客户成功呼叫

字段 内容
优先级 P0
目的 验证 IP 鉴权客户从 T 发起呼叫时,A 能识别客户网关、匹配业务前缀、选择线路组落地,并生成完整 CDR。
前置条件 A/B/T 服务 activecgw_call_ip_001 启用;T 源 IP 位于网关 sourceIps;落地 UAS 可接听。
测试数据 主叫 02160010001;被叫 67113800138000;预期真实被叫 13800138000Call-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 OKBYE 返回 200 OK;A 出局到预期落地网关;B 生成 1 条 raw CDR 和 1 条 rated CDRCDR 包含 customerId、customerGatewayId、lineGroupId、vendorGatewayId、businessPrefix、rawCallee、callee、landingCaller、landingCallee。
数据检查 raw CDR ratingStatus=RATEDrated CDR billSec、费用和余额扣减正确;configVersion 为当前 active versionHOMER 中可查到入局和出局信令。
安全检查 非白名单源 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_001source_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 CDRratingStatus=SKIPPEDcustomerFee=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_001backup vgw_call_backup_001 启用。
步骤 1. 正常呼叫确认 primary 被选中。2. 禁用 primary 并发布。3. 再次呼叫。4. 查询 CDR、HOMER 和落地侧日志。
预期结果 禁用前 CDR 记录 primary;禁用后新呼叫记录 backup;SIP 呼叫成功;供应商成本按 backup 费率计算。
数据检查 CDR vendorGatewayIdvendorIdlineGroupId 正确;配置版本变化可追踪。
安全检查 故障转移不绕过客户鉴权、主叫匹配、业务前缀匹配和余额保护。

CALL-007 主叫/被叫策略不匹配

字段 内容
优先级 P1
目的 验证客户网关主叫前缀、业务前缀和策略匹配失败时不会误路由。
前置条件 客户网关设置 callerMatchMode=PREFIXES 且只允许 021calleeMatchMode=BUSINESS_PREFIXES 只允许 671;策略设置特定主被叫。
测试数据 主叫 01060010001;被叫 67213800138000 或不满足策略的号码。
步骤 1. 发起主叫不匹配呼叫。2. 发起业务前缀不匹配呼叫。3. 发起策略不匹配呼叫。4. 查询 CDR 和日志。
预期结果 呼叫被拒绝或进入明确失败路径;不产生 rated CDRraw CDR 如存在,placeholder ID 归一为 null,失败原因可追踪。
数据检查 customer_gateway_policy_id=no_policy_match 等占位值入库时转换为 null;余额不扣减。
安全检查 不匹配失败不暴露可用前缀或策略详情。

CDR-001 成功话单计费

字段 内容
优先级 P0
目的 验证成功 2xx 通话按落地网关计费周期向上取整,并扣减客户余额。
前置条件 落地网关 billingCycleSec=6cycleRate=0.012000;客户余额 10.000000
测试数据 CDR duration_sec=28sip_code=200
步骤 1. 通过真实呼叫或注入 CDR Stream 生成 28 秒成功事件。2. 等待 CDR Worker 消费。3. 查询 raw/rated CDR 和客户余额。
预期结果 cycles=5billSec=30customerFee=0.060000vendorCost=0.060000grossProfit=0.000000;客户余额变为 9.940000
数据检查 raw CDR ratingStatus=RATEDrated CDR customerRate.source=S23_MINIMAL_MIRROR_VENDOR_RATEvendorRate.source=vendor_gateway;客户 updatedBy=worker-cdrversion 增加。
安全检查 计费不写充值流水;余额扣减在数据库事务内完成。

计费边界矩阵:

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=486503404duration_sec=0answered_at=none
步骤 1. 模拟忙线、拒接、无路由或注入失败 CDR。2. 等待 Worker 消费。3. 查询 raw/rated CDR 和余额。
预期结果 raw CDR 创建成功;ratingStatus=SKIPPED;不创建 rated CDR 或 rated 金额为 0,按实现固定;客户余额不变。
数据检查 hangupReasonsipCodestartedAt/endedAt 保存;失败 CDR 可在话单中心筛选。
安全检查 失败话单不触发录音播放越权,不泄露内部路由细节。

CDR-003 重复 CDR 幂等

字段 内容
优先级 P0
目的 验证同一 event_id 重复进入 Redis Stream 时,raw/rated CDR 和余额扣减只发生一次。
前置条件 Redis Stream 和 CDR Worker active。
测试数据 两条完全相同 CDRevent_id=evt-cdr-003-001duration_sec=28sip_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_idschema_version=999duration_sec=-1sip_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_idevent_idcall_iderrorpayloaddeadlettered_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 回到 0deadletter 无新增;余额只扣一次。
安全检查 可恢复错误日志不包含数据库密码。

CDR-006 缺失计费对象

字段 内容
优先级 P1
目的 验证 CDR 中缺少客户或落地网关等关键计费对象时,系统不会错误扣费。
前置条件 CDR Worker active。
测试数据 customer_id=unknownvendor_gateway_id=unknowncustomer_gateway_policy_id=no_policy_matchvendor_gateway_id 指向不存在网关。
步骤 1. 分别注入上述 CDR。2. 等待 Worker 消费。3. 查询 raw/rated CDR 和余额。
预期结果 placeholder ID 入库时归一为 null;缺少 billable customer/vendor gateway 时 raw 标记 SKIPPEDFAILED,按实现固定;不扣余额。
数据检查 noneunknownno_active_versionno_policy_matchsingle_gateway 等占位不作为外键写入;失败原因可定位。
安全检查 不因无效外键导致 Worker 崩溃或队列停滞。

CDR-007 号码归属地识别

字段 内容
优先级 P1
目的 验证号码库快照参与呼叫归属地和运营商识别,并写入 CDR。
前置条件 号码库已导入并发布;包含手机号段 1380013 和区号/运营商规则。
测试数据 被叫 67113800138000;预期 normalized_callee=13800138000
步骤 1. 发起成功呼叫。2. 查询 raw CDR payload 和话单列表。3. 使用 cityCodecarrier 筛选话单。
预期结果 CDR 包含 calleeCityCodecalleeCityNamecalleeProvinceNamecalleeOperatorcalleeNumberType;话单筛选能命中该记录。
数据检查 手机号、固话、国际号码分别识别为 MOBILELANDLINEINTERNATIONALUNKNOWN;未知号码不应报错。
安全检查 号码库未命中时降级为 UNKNOWN,不影响话单入库。

CDR-008 话单查询和详情

字段 内容
优先级 P1
目的 验证话单中心 API 查询条件、分页、详情和权限。
前置条件 已存在多条不同主叫、被叫、客户网关、落地网关、城市、运营商和时间的话单。
测试数据 caller=02160010001callee=13800138000customerGatewayId=cgw_call_ip_001vendorGatewayId=vgw_call_primary_001cityCode=340100carrier=MOBILEtake=20skip=0
步骤 1. 使用有 cdr.view 权限账号请求 GET /api/v2/cdrs。2. 分别组合号码、网关、城市、运营商和时间范围查询。3. 请求 GET /api/v2/cdrs/:id。4. 使用无权限账号请求。
预期结果 有权限查询返回匹配数据和分页信息;详情包含 raw/rated、录音、业务前缀、落地、费用等字段;无权限返回 403。
数据检查 查询默认 take=100,最大 500skip 最大 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=0take=501skip=-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=0deadletter=0。
数据检查 每条 raw CDR event_id 唯一;客户 version 增加符合更新次数或按实现固定。
安全检查 并发下不产生负外键、重复 rated 或事务死锁长时间阻塞。

8.6 录音、播放与质检

本节覆盖 RTPEngine 录音 ready 文件、Recording Worker 搬运、校验、源文件清理、录音播放鉴权、Nginx X-Accel/Range、质检抽样、评分保存和审计。后端接口统一以 /api/v2 为前缀。

公共前置数据:

  • 录音查看账号 recording_viewer_001:包含 cdr.viewrecordings.play
  • 质检账号 quality_ops_001:包含 quality.viewquality.managerecordings.play
  • 只读质检账号 quality_viewer_001:包含 quality.view,不包含 quality.managerecordings.play
  • 至少 1 条成功通话 CDR,关联 READY 录音 rec_ready_seed_001storageKey 指向 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 状态为 READYstorageKey 为安全相对路径;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 返回 206Content-RangeContent-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.5result=ISSUEissueTags=["noise","script"]remark=QA-002 review
步骤 1. 打开质检中心并选择录音。2. 播放录音。3. 提交评分、结果、标签、备注。4. 刷新列表和详情。5. 查询审计。
预期结果 保存成功;录音 review 状态更新;列表展示评分、结果、质检人和时间;审计写入 quality 模块成功记录。
数据检查 重复保存按更新逻辑覆盖或产生历史版本,规则需固定;录音文件不被修改。
安全检查 quality.manage 保存返回 403;无 recordings.play 不应播放但可否评分按产品规则固定。

QA-003 评分边界校验

字段 内容
优先级 P1
目的 验证评分、结果枚举、标签和备注长度校验。
前置条件 有可质检录音。
测试数据 合法评分 010088.55;非法评分 -1100.01abc;非法 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.viewactive_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:59Z2026-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、时长和 SIP 信令状态。
前置条件 A 侧存在进行中 Dialog 或使用 MI mock。
测试数据 一通进行中呼叫,含 Contact、SDP、dialog ID。
步骤 1. 发起长持话呼叫。2. 请求当前通话接口或打开页面。3. 对比 OpenSIPS MI 输出。
预期结果 列表显示 callId/dialogId、caller、callee、sourceIp、landingIp、durationSec、startedAt、sipStateSIP 状态按 Dialog 归一化为 INVITE180 Ringing183 Session Progress200 OKBYE / Terminating 等可读状态;自动刷新不闪退。
数据检查 解析 IPv4/IPv6、Contact 和 SDP 中 IP 正确;无 Dialog 时返回空列表。
安全检查 active_calls.view 不可查看。

ACT-002 安全挂断

字段 内容
优先级 P0
目的 验证有权限用户可通过 OpenSIPS MI 挂断进行中通话,并处理已结束竞态。
前置条件 有进行中 Dialogactive_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=customersaction=createresult=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-001SIP 密码 SipSecret-001
步骤 1. 执行敏感操作。2. 查询审计详情和应用日志。3. 全文搜索敏感明文。
预期结果 敏感字段显示 [REDACTED] 或不出现;明文密码、refresh token、sipPassword、sipHa1 不可检索。
数据检查 脱敏不影响非敏感字段排障。
安全检查 嵌套对象和数组内敏感字段也脱敏。

8.8 前端页面与交互

本节覆盖 Web 登录、菜单、页面切换、错误/空状态、表单、权限按钮、构建产物和浏览器控制台。建议使用真实 B 环境或本地 mock API 两种方式执行。

公共前置数据:

  • 管理员账号:具备全部核心页面权限。
  • 只读账号:仅具备 dashboard.viewcustomers.view
  • 各核心列表至少有 1 条数据,同时准备空数据环境或 mock 返回空数组。
  • 浏览器打开 devtools,记录 console error/warn 和 network 失败。

WEB-001 登录态启动

字段 内容
优先级 P0
目的 验证 Web 首屏、验证码、登录、refresh 和 Dashboard 初始化。
前置条件 API readyWeb 静态资源可访问。
测试数据 管理员账号。
步骤 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. 执行 pnpm test:remote-web-ui,由 Playwright 使用管理员账号登录并逐个点击核心菜单。2. 每个页面加载后点击一个安全主操作(如编辑、查看详情、刷新)以覆盖弹窗/抽屉渲染。3. 记录页面标题、可见文本长度、console error 和 pageerror。4. 只读账号登录检查菜单。5. 浏览器刷新每个关键页面。
预期结果 每页标题正确;菜单切换和安全主操作均无 runtime error;无空白页;不出现 API 数据不可用;权限菜单和按钮符合权限矩阵;刷新不丢状态;任一 console error/pageerror 均判失败。
数据检查 只请求当前权限允许的全局 API,避免批量 403。
安全检查 手动输入无权限路由显示无权限或跳转。

WEB-006 构建产物发布

字段 内容
优先级 P0
目的 验证前端 build 产物被正确发布到 B 当前 release,Nginx 首页引用新资源。
前置条件 本地 build 成功;B 当前 release 明确;有回滚点。
测试数据 新 JS/CSS hash。
步骤 1. 执行 pnpm lint,确认 apps/web/src/** 前端源码未出现未定义标识。2. 执行 build。3. 发布 dist。4. curl -k https://127.0.0.1/ 检查资源引用。5. 执行 pnpm test:remote-web-ui。6. 浏览器强刷首页。
预期结果 前端 lint 覆盖源码并通过;首页引用新 JS/CSS;旧资源不被引用;Nginx 返回 200;页面可登录;核心菜单逐页切换无空白、无 console error/pageerror。
数据检查 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/buildreleaseId 已冻结。
步骤 1. 执行 pnpm release:artifact -- --release-id <releaseId>。2. 检查 dist/releases/<releaseId>.tar.gz.sha256。3. 解包到临时目录检查 RELEASE_MANIFEST.jsonpublic/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 entriesSHA256 校验通过;正式发布包不得由 Windows 原生 node_modules 生成。
数据检查 解包目录文件数量合理,关键路径完整;.env、私钥、明文密码、.codex-private 不进入 artifact。
安全检查 脚本输出不回显数据库密码、Redis 密码、JWT secret、SSH 密钥。

OPS-001 B preflight

字段 内容
优先级 P0
目的 验证 B 当前 release 和关键服务满足发布/验收基线。
前置条件 B 已部署 releaseSSH 可达。
步骤 1. 执行 lisglosips-release-preflight.sh。2. 检查 systemd 服务。3. 请求 API ready 和 HTTPS 首页。
预期结果 MySQL、Redis、Nginx、API、CDR Worker、Recording Worker、Config Publisher、HEP、Prometheus、Grafana 均 activeAPI ready 返回 okNginx 配置测试通过。
数据检查 current symlink 指向预期 release;最近备份目录可见。
安全检查 输出不包含密码、token、私钥。

OPS-002 MySQL 备份校验

字段 内容
优先级 P0
目的 验证 MySQL 备份完整、可校验、元数据可追踪。
前置条件 B MySQL 正常,有业务数据。
步骤 1. 触发 MySQL 备份。2. 检查备份目录、metadata.tsvSHA256SUMS。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 已切到新 releaseA 配置和 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。
预期结果 回滚后服务 activeAPI ready okWeb 可访问;核心接口兼容;可再切回。
数据检查 无数据库 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/authsecurityaudit:回归 AUTH、AUD、WEB 权限相关用例。
  • customer-gatewaysbusiness-prefixesworker-config-publisher:回归 GW、CALL 业务前缀和配置发布。
  • worker-cdrpackages/redis:回归 CDR、充值幂等、FAIL-002/FAIL-003。
  • worker-recordingrecordingsquality:回归 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.jsxpagespermissions.js:回归 WEB-001 至 WEB-005、AUTH-007、AUTH-008。

11. 当前已知风险与补充建议

  • Redis 恢复后 OpenSIPS 热路径曾出现需要重启 opensips 才恢复的问题。上线前应补充 Redis 断连重连专项测试,并增加明确指标/告警。
  • 当前真实 80 万手机号段尚未导入。号码库性能、导入耗时、批量校验和 CDR 归属地准确性需单独做数据量级测试。
  • 真实 B 浏览器登录态下的逐页按钮、真实录音播放和 Nginx X-Accel 链路仍需人工复核。
  • 阿里云迁移前必须重跑网络安全组、正式 TLS、数据盘、备份恢复、故障注入和灰度呼叫,不应直接沿用本地 KVM 结论。
  • 建议后续增加 Playwright 前端冒烟:登录、菜单遍历、权限账号、关键表单、录音播放入口,降低页面空白类问题复发概率。