Files
lisglosips/SOFTSWITCH_PLATFORM_DESIGN_V2.md
T

1436 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# LisgloSIPS V2 项目设计与实施文档
> 中文名:聆界SIP管理平台
> 文档版本:V2.2
> 编制日期:2026-06-20
> 文档状态:S30 后二期需求变更持续补充
> 依据:服务器架构图、`SOFTSWITCH_PLATFORM_DESIGN_V1.md`、当前 React Web Demo、`OPENSIPS_INSTALL_NOTES.md`
## 1. 文档目的
V2 的目标是把当前纯前端原型转化为可部署、可联调、可上线的软交换运营平台。本文不仅描述产品功能,还给出服务器部署、软件安装、后端代码、OpenSIPS/RTPEngine 对接、数据库、Redis、录音迁移、测试和上线方案。
本文作为下一阶段执行基线,后续数据库 DDL、OpenAPI、OpenSIPS 脚本和部署脚本应从本文拆分并版本化管理。
## 2. V2 范围
### 2.1 本期实施页面
以下菜单已在 Web Demo 中完成交互设计,纳入 V2 后端和联调范围:
| 菜单 | V2 实施内容 |
| --- | --- |
| 概览 Dashboard | 通话、接通率、消费、成本、毛利、注册、节点、异常网关、质检指标 |
| 客户管理 | 客户新增、编辑、启停、余额、授信、充值、网关数量、受控删除 |
| 客户网关管理 | IP/SIP 注册认证、启停、删除、多个策略、优先级、主被叫匹配、线路组绑定 |
| 充值记录 | 客户充值、供应商充值、余额前后值、操作人、备注;仅记录页面人工充值,不记录话单消费扣费 |
| 供应商管理 | 供应商账户、余额、授信、充值、落地网关数量、受控删除 |
| 落地网关管理 | 认证、并发、CPS、禁呼时段、编码、号码转换、计费周期、费率、启停、删除 |
| 落地线路组 | 线路组、组内网关、优先级、并发汇总、使用客户网关数量、受控删除 |
| 号码库 | 手机号码库、城市区号、运营商号码段规则;为话单归属地/运营商和落地网关屏蔽地区提供基础数据 |
| 当前通话 | OpenSIPS Dialog 实时列表、呼叫方 IP、落地 IP、自动刷新、强制挂断 |
| 话单中心 | 话单查询、挂断原因、费用、录音、信令入口、详情 |
| 质检中心 | 抽检规则、录音列表、连续播放、自动播放、问题标注、评分 |
| 用户管理 | 用户新增、编辑、启停、重置密码、角色绑定 |
| 角色与权限 | 角色、权限矩阵、角色用户数、内置角色保护 |
| 操作日志 | 条件查询、结果、详情、导出 |
### 2.2 延期页面
以下菜单在 Web Demo 中曾标记为“待设计”,S30 后二期变更要求在导航中隐藏,不向运营用户展示入口:
- 费率与计费
- SIP 运维
- 监控告警
- 系统设置
说明:页面延期不代表底层能力完全取消。V2 仍需实现:
- 最小计费引擎,用于生成话单客户费用、成本费用并更新余额。
- Prometheus/Grafana 基础设施监控,但不接入自研“监控告警”页面。
- OpenSIPS/RTPEngine 配置文件和环境变量管理,但不开放自研“系统设置”页面。
- HOMER 信令查询入口,但不开发自研“SIP 运维”页面。
### 2.3 明确不做
- 客户自助门户。
- 多 OpenSIPS 节点和通话级高可用。
- 完整账单、发票、对账和复杂费率管理页面。
- 自研可视化 SIP Trace、监控告警和系统配置页面。
- Kubernetes。双机阶段使用 systemd、Nginx 和 Docker Compose 的有限组合。
## 3. 总体架构
### 3.1 服务器规划
环境分为两个阶段:
- 当前开发阶段:A、B、T 均为本地 KVM 虚拟机,通过 Tailscale 专网联调。
- 开发完成后:将 A、B 部署到阿里云,按下表建立 VPC、公网入口和安全组;T 保留为外部 SIP 测试端。
下表为最终阿里云生产目标,不代表当前本地开发机已经位于阿里云:
| 服务器 | 规格 | 定位 | 公网暴露 |
| --- | --- | --- | --- |
| Server A | 阿里云计算型 4C8G | 核心通信网关 | SIP `15060/UDP`、RTP UDP 端口段 |
| Server B | 阿里云通用型 4C16G | 业务、缓存、数据库、录音、监控中心 | `443/TCP`,可选 `80/TCP` 跳转 HTTPS |
两台服务器必须位于同一 VPC、同一可用区或低时延可用区,使用私网 IP 通信。目标私网 RTT 小于 1 ms。
本地开发阶段的实测网络、端口矩阵和未来阿里云安全组清单见 `docs/infra-check.md`
### 3.2 逻辑拓扑
```mermaid
flowchart LR
UA["客户 SIP / 上游线路"] -->|"SIP 15060/UDP"| OS["Server A: OpenSIPS"]
UA <-->|"RTP UDP 端口段"| RTP["Server A: RTPEngine"]
OS <-->|"call-time Redis 查询"| REDIS["Server B: Redis"]
OS -->|"Redis Stream: CDR"| REDIS
OS -->|"HEP/9060 私网"| HEP["Server B: Heplify-server + HOMER"]
RTP --> TMP["Server A: /dev/shm/voip_rec"]
WORKER["Server B: Recording Worker"] -->|"私网拉取 ready 文件"| TMP
WORKER --> SSD["Server B: 录音数据盘"]
API["Server B: LisgloSIPS API"] --> MYSQL["Server B: MySQL"]
API --> REDIS
CDRW["Server B: CDR/Billing Worker"] --> REDIS
CDRW --> MYSQL
WEB["React Web + Nginx"] --> API
EXPORTER["Server A: Exporters"] --> PROM["Server B: Prometheus + Grafana"]
```
### 3.3 Server A 职责
- OpenSIPS:SIP 鉴权、客户识别、策略匹配、路由、Dialog/CDR 事件。
- RTPEngine:媒体锚定、NAT 穿透、双向录音、内核态转发。
- `/dev/shm/voip_rec`3 GiB tmpfs 录音缓冲区。
- HEP 发送端:向 Server B 发送信令镜像。
- Node Exporter 和自定义 textfile 指标。
- Fail2ban、nftables、OpenSIPS `pike`/`ratelimit` 防护。
“无状态”定义为不在 Server A 持久化业务主数据。OpenSIPS 运行时仍会维护事务和 Dialog 内存状态,服务重启会影响现有通话,V2 单节点接受此限制。
### 3.4 Server B 职责
- Nginx、React 静态资源、HTTPS 和 API 反向代理。
- LisgloSIPS API、CDR Worker、Recording Worker、Config Publisher。
- Redis:呼叫热数据、配置版本、锁、Redis Streams。
- MySQL:业务主数据、充值流水、话单、质检、权限、审计。
- 录音数据盘:保存迁移后的录音文件。
- Heplify-server、HOMER UI 及其独立存储。
- Prometheus、Grafana 及必要 exporters。
## 4. 架构图必须修正的两点
### 4.1 RTP 端口不能只开放 SIP 端口
架构图写明 Server A 公网仅暴露 `15060/UDP`,这不足以完成 RTP 媒体通信。若 RTPEngine 负责公网媒体锚定,安全组和主机防火墙还必须开放 RTPEngine 配置的 UDP 端口范围,例如:
```text
SIP: 15060/UDP
RTP: 30000-40000/UDP
```
端口范围应根据并发测算缩小,并尽量限制来源运营商或客户 IP 段。若上游 RTP 来源不可预知,需要保留公网 UDP 范围并强化速率限制和监控。
### 4.2 CDR 使用 Redis Stream
架构图中的 `queue:cdr_payload` 不应实现为无法确认消费的普通 Redis List。V2 使用 Redis Stream
```text
stream:cdr_payload
consumer group: billing-workers
```
要求支持:
- 消息 ID 和业务幂等键。
- `XREADGROUP` 消费。
- MySQL 提交成功后 `XACK`
- Pending 消息重领。
- 死信 Stream。
- Redis 暂时不可用时 Server A 本地受限缓冲或告警降级。
## 5. 技术栈与下载清单
### 5.1 版本原则
- 服务器统一 Ubuntu 24.04 LTS。
- OpenSIPS 使用项目已验证的 3.6.x 分支。
- 其他软件选择受支持稳定版,禁止部署时无条件升级“最新版”。
- 所有版本写入 `versions.env`、部署脚本或容器镜像标签。
- 先在预生产执行安装、通话和性能验证,再用于生产。
### 5.2 Server A 软件
| 软件 | 用途 | 安装来源 |
| --- | --- | --- |
| OpenSIPS 3.6.x | SIP 核心 | https://apt.opensips.org/ |
| opensips-cli | 管理与检查 | OpenSIPS APT |
| OpenSIPS Redis/HTTP/JSON 模块 | Redis 热路径、MI、事件 | OpenSIPS APT |
| RTPEngine daemon + kernel module | RTP 转发与录音 | https://github.com/sipwise/rtpengine |
| Heplify client 或 OpenSIPS tracer/HEP | 信令镜像 | https://github.com/sipcapture/heplify |
| Node Exporter | 主机指标 | https://prometheus.io/docs/guides/node-exporter/ |
| Fail2ban、nftables | 主机防护 | Ubuntu APT |
| chrony | 时间同步 | Ubuntu APT |
建议 OpenSIPS 包:
```text
opensips
opensips-cli
opensips-redis-module
opensips-http-modules
opensips-json-module
opensips-tls-module
opensips-auth-modules
opensips-mysql-dbschema
```
实际包名以 OpenSIPS 3.6 Ubuntu 24.04 仓库为准,安装脚本执行前用 `apt-cache search opensips` 校验。
### 5.3 Server B 软件
| 软件 | 用途 | 安装来源 |
| --- | --- | --- |
| Node.js LTS | API 和 Worker | https://nodejs.org/en/about/previous-releases |
| pnpm | Node.js 包管理 | Corepack |
| Nginx | Web、HTTPS、反向代理 | https://nginx.org/en/linux_packages.html |
| Redis | 热数据与 Streams | https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/ |
| MySQL | 业务持久化 | https://dev.mysql.com/downloads/repo/apt/ |
| Heplify-server + HOMER UI | 信令接收和排障 | https://github.com/sipcapture/heplify-server |
| Prometheus | 指标存储 | https://prometheus.io/docs/prometheus/latest/installation/ |
| Grafana | 监控大盘 | https://grafana.com/docs/grafana/latest/setup-grafana/installation/debian/ |
| mysqld_exporter、redis_exporter | 数据库与缓存指标 | Prometheus Community |
| Docker Engine/Compose | 仅用于 HOMER 组合服务 | https://docs.docker.com/engine/install/ubuntu/ |
| ffmpeg | 录音格式校验与转换 | Ubuntu APT |
HOMER 数据与业务 MySQL 必须逻辑隔离。优先使用 HOMER 官方 Compose 所要求的数据库,不把 HEP 海量信令写入业务库。
## 6. 网络、安全组和端口
### 6.1 Server A 入站
| 端口 | 来源 | 说明 |
| --- | --- | --- |
| `15060/UDP` | 客户/供应商允许列表 | SIP |
| `30000-40000/UDP` | RTP 对端或允许网段 | RTPEngine 媒体,最终按容量调整 |
| `<SSH_ADMIN_PORT>/TCP` | 堡垒机或管理 VPN | SSH 管理端口,不对全网开放;保留密钥登录 |
### 6.2 Server B 入站
| 端口 | 来源 | 说明 |
| --- | --- | --- |
| `443/TCP` | 管理端用户 | LisgloSIPS HTTPS |
| `80/TCP` | 可选 | 跳转 HTTPS,不承载登录 |
| `9060/UDP` | Server A 私网 IP | HEP |
| `<SSH_ADMIN_PORT>/TCP` | 堡垒机或管理 VPN | SSH 管理端口;保留密钥登录 |
Redis `6379`、MySQL `3306`、Prometheus `9090`、Grafana `3000`、应用端口、HOMER 内部端口不得直接暴露公网。Redis 和 MySQL至少绑定私网 IP,并通过安全组仅允许明确来源;同机服务优先使用 `127.0.0.1`
### 6.3 安全要求
- 全部服务器启用 chrony,CDR、HOMER 和审计日志时间必须一致。
- Web 强制 HTTPS、HSTS、SameSite Cookie 和 CSRF 防护。
- 管理员密码使用 Argon2id;不记录明文密码。
- SIP 注册密码按 OpenSIPS Digest 需要保存 HA1 或受控密文,不直接返回前端。
- MI HTTP 只绑定 Server A `127.0.0.1`
- 所有生产 Secret 放入权限为 `0600` 的环境文件或云 Secret 服务。
- 操作日志不得记录密码、完整 Authorization、Cookie、录音签名 URL。
## 7. 基础环境部署
### 7.1 阿里云资源
1. 创建 VPC 和私有子网。
2. 创建 Server A 4C8G 计算型实例和 Server B 4C16G 通用型实例。
3. Server B 挂载独立 ESSD 数据盘,业务数据库、录音和监控数据使用独立目录或分区。
4. 配置安全组、EIP、DNS 和快照策略。
5. 建议先创建同规格预生产环境,至少在上线前保留 3 天完整演练时间。
### 7.2 两台服务器通用初始化
```bash
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y curl wget ca-certificates gnupg jq git unzip chrony \
nftables fail2ban vim htop iotop sysstat lsof net-tools tcpdump
sudo timedatectl set-timezone Asia/Shanghai
sudo systemctl enable --now chrony nftables fail2ban
```
生产环境推荐系统内部统一存 UTC,前端按 `Asia/Shanghai` 展示;若操作系统使用上海时区,数据库字段仍需明确 UTC 语义。
### 7.3 Server A tmpfs
```bash
sudo mkdir -p /dev/shm/voip_rec
sudo chown rtpengine:rtpengine /dev/shm/voip_rec
sudo chmod 0750 /dev/shm/voip_rec
```
`/etc/fstab`
```fstab
tmpfs /dev/shm/voip_rec tmpfs rw,nosuid,nodev,noexec,size=3G,mode=0750 0 0
```
3 GiB 缓冲不是长期存储。按双向 G.711 原始音频约 16 KiB/s 粗略估算,只能容纳约 53 个“并发小时”,未计算容器和文件头开销。100 路持续录音时缓冲可能不到 35 分钟。Recording Worker 建议 5-15 秒扫描一次,使用量超过 70% 告警,超过 85% 时停止接受新的非强制录音或触发紧急迁移。
### 7.4 Server B 数据盘目录
```text
/data/mysql
/data/redis
/data/recordings/YYYY/MM/DD
/data/prometheus
/data/grafana
/data/homer
/data/backups
```
数据盘挂载参数、文件系统和 IOPS 规格需根据压测结果确认。录音目录和 MySQL 不建议共享同一个容易打满的根分区。
## 8. OpenSIPS 与 RTPEngine 实施
### 8.1 OpenSIPS 呼叫路径
```text
INVITE
-> 基础合法性与防扫描检查
-> IP 或 SIP Digest 认证
-> Redis 查询客户、客户网关、状态、余额门槛
-> Redis 查询客户网关策略(主叫条件 AND 被叫条件,按优先级)
-> 获得落地线路组
-> 识别被叫号码归属地级市和运营商
-> 按组内优先级、启用状态、并发/CPS、屏蔽地区选择落地网关
-> 执行主被叫前缀转换、时段和编码限制
-> RTPEngine offer/answer
-> 转发到供应商
-> BYE/失败时生成标准化 CDR 事件
```
### 8.2 呼叫前原子检查
Redis Lua 脚本一次完成:
- 客户是否启用。
- 客户网关是否启用。
- IP/账号是否匹配。
- 余额加授信是否大于最小通话门槛。
- 网关 CPS 和并发是否超限。
- 黑名单是否命中。
- 返回配置版本、线路组和路由策略。
金额禁止使用 Redis 浮点数,统一使用整数“微元”或其他固定精度最小单位。
### 8.3 RTPEngine
- 使用内核转发模块并在启动时校验。
- 控制接口只在本机或私网监听。
- RTP 端口段与安全组保持一致。
- 录音先写 `.part`,完成后原子重命名为 `.ready`
- 文件名包含日期、Call-ID 哈希和方向,禁止直接拼接未清洗 SIP Header。
### 8.4 CDR 标准事件
```json
{
"event_id": "uuid",
"idempotency_key": "call_id + ended_at 或 event_id",
"call_id": "sip-call-id",
"node_id": "a1",
"opensips_instance": "opensips-a1",
"ingress_a_ip": "100.90.90.90",
"rtpengine_node": "a1",
"customer_id": "C1001",
"customer_gateway_id": "CGW-001",
"source_ip": "10.10.1.11",
"caller": "02160010001",
"callee": "13800138000",
"callee_city_code": "310000",
"callee_city_name": "上海",
"callee_province_name": "上海",
"callee_operator": "MOBILE",
"vendor_id": "V1001",
"vendor_gateway_id": "VGW-001",
"line_group_id": "LG-001",
"started_at": "2026-06-20T02:14:22.123Z",
"answered_at": "2026-06-20T02:14:27.010Z",
"ended_at": "2026-06-20T02:17:30.445Z",
"duration_sec": 183,
"sip_code": 200,
"hangup_reason": "NORMAL_CLEARING",
"recording_key": "2026/06/20/xxx.ready",
"config_version": 1042
}
```
`event_id``call_id + ended_at` 作为幂等依据。任何重试不得重复扣费或重复写充值/余额流水。
## 9. Redis 设计
### 9.1 Key 规范
```text
cfg:active_version
cfg:customer:{customer_id}
cfg:customer_gateway:{gateway_id}
cfg:customer_gateway:{gateway_id}:policies
cfg:vendor_gateway:{gateway_id}
cfg:line_group:{group_id}:items
auth:ip:{ip}
auth:sip:{username}
balance:customer:{customer_id}
blacklist:caller
blacklist:callee
stream:cdr_payload
stream:cdr_deadletter
stream:recording_jobs
lock:cdr:{event_id}
```
### 9.2 配置发布
1. API 在 MySQL 事务中写业务表和 `outbox_events`
2. Config Publisher 读取 outbox,生成完整配置快照。
3. 使用临时版本 Key 写入 Redis。
4. 校验数量和哈希后原子切换 `cfg:active_version`
5. 发布成功后更新 outbox 状态。
6. 保留上一个版本,故障时一键回滚。
禁止 API 先改 Redis、后改 MySQL。MySQL 是唯一业务事实源,Redis 是呼叫热路径副本。
### 9.3 Redis 持久化
- 启用 AOF `appendfsync everysec`
- 配置 RDB 快照。
- 限制 `maxmemory`,配置前评估淘汰策略;核心配置和队列不得被随机淘汰。
- 监控 Stream 长度、Pending 数、消费延迟、内存和连接数。
## 10. MySQL 数据模型
### 10.1 业务核心表
```text
customers
customer_gateways
customer_gateway_policies
customer_recharges
vendors
vendor_recharges
vendor_gateways
vendor_gateway_blocked_regions
vendor_gateway_forbidden_periods
vendor_gateway_codecs
vendor_gateway_prefix_rules
landing_line_groups
landing_line_group_items
geo_cities
phone_number_segments
carrier_prefix_rules
raw_cdrs
rated_cdrs
recordings
quality_sampling_rules
quality_reviews
users
roles
permissions
user_roles
role_permissions
audit_logs
outbox_events
idempotency_keys
```
### 10.2 关键约束
- ID 使用业务前缀 + 雪花 ID/UUID,禁止依赖前端生成。
- 金额使用 `DECIMAL(20,6)`,应用层不得使用 JavaScript 浮点直接计算金额。
- 充值使用不可变流水,余额变化必须与流水在同一事务提交。
- 网关策略唯一索引至少包含 `gateway_id + priority`
- 线路组成员唯一索引包含 `line_group_id + vendor_gateway_id`
- CDR 使用 `event_id` 唯一索引。
- CDR 需要保存被叫号码解析出的地级市、省份和运营商快照,避免后续号码库更新影响历史话单解释。
- 所有管理表包含 `created_at``updated_at``created_by``updated_by` 和乐观锁版本号。
- 时间存 UTCAPI 返回 ISO 8601。
- 删除优先软删除;充值、CDR、审计日志不允许物理删除。
### 10.3 余额事务
充值事务:
```text
锁定客户/供应商账户行
-> 读取充值前余额
-> 插入充值流水
-> 更新余额
-> 插入审计日志/Outbox
-> 提交
```
CDR 扣费事务:
```text
检查 event_id 幂等
-> 写 raw_cdr
-> 计算最小计费结果
-> 写 rated_cdr
-> 锁定客户余额
-> 扣费并写余额流水
-> ACK Redis Stream
```
## 11. 后端工程设计
### 11.1 技术选型
- TypeScript + Node.js LTS。
- NestJS + Fastify Adapter。
- Prisma 管理 MySQL Schema 和迁移。
- ioredis 访问 Redis 和 Streams。
- OpenAPI/Swagger 生成 API 契约。
- Pino 输出结构化 JSON 日志。
- Vitest/Jest 做单元测试,Supertest 做 API 集成测试。
- systemd 管理 API 和 WorkerHOMER 可使用 Docker Compose。
### 11.2 推荐目录
```text
lisglosips/
├── apps/
│ ├── web/
│ ├── api/
│ ├── worker-cdr/
│ ├── worker-recording/
│ └── worker-config-publisher/
├── packages/
│ ├── contracts/
│ ├── domain/
│ ├── database/
│ ├── redis/
│ ├── auth/
│ └── observability/
├── prisma/
│ ├── schema.prisma
│ └── migrations/
├── deploy/
│ ├── server-a/
│ ├── server-b/
│ ├── nginx/
│ ├── systemd/
│ └── homer/
├── opensips/
│ ├── opensips.cfg
│ ├── routes/
│ └── scripts/
├── docs/
│ ├── openapi.yaml
│ ├── redis-keys.md
│ └── runbooks/
└── tests/
```
### 11.3 后端模块
```text
AuthModule
DashboardModule
CustomersModule
CustomerGatewaysModule
CustomerGatewayPoliciesModule
RechargesModule
VendorsModule
VendorGatewaysModule
LandingLineGroupsModule
CdrModule
RecordingsModule
QualityModule
UsersModule
RolesModule
AuditModule
ConfigPublisherModule
HealthModule
```
### 11.4 API 清单
```text
POST /api/v2/auth/login
POST /api/v2/auth/refresh
POST /api/v2/auth/logout
GET /api/v2/dashboard/summary
GET /api/v2/dashboard/trends
GET /api/v2/customers
POST /api/v2/customers
PATCH /api/v2/customers/:id
POST /api/v2/customers/:id/recharges
GET /api/v2/customer-gateways
POST /api/v2/customer-gateways
PATCH /api/v2/customer-gateways/:id
POST /api/v2/customer-gateways/:id/enable
POST /api/v2/customer-gateways/:id/disable
GET /api/v2/customer-gateways/:id/policies
POST /api/v2/customer-gateways/:id/policies
PATCH /api/v2/customer-gateway-policies/:id
DELETE /api/v2/customer-gateway-policies/:id
POST /api/v2/customer-gateways/:id/policies/reorder
GET /api/v2/vendors
POST /api/v2/vendors
PATCH /api/v2/vendors/:id
POST /api/v2/vendors/:id/recharges
GET /api/v2/recharges
GET /api/v2/vendor-gateways
POST /api/v2/vendor-gateways
PATCH /api/v2/vendor-gateways/:id
POST /api/v2/vendor-gateways/:id/enable
POST /api/v2/vendor-gateways/:id/disable
GET /api/v2/landing-line-groups
POST /api/v2/landing-line-groups
PATCH /api/v2/landing-line-groups/:id
POST /api/v2/landing-line-groups/:id/items
DELETE /api/v2/landing-line-groups/:id/items/:gatewayId
POST /api/v2/landing-line-groups/:id/items/reorder
GET /api/v2/cdrs
GET /api/v2/cdrs/:id
GET /api/v2/cdrs/:id/recording
GET /api/v2/cdrs/:id/signaling-link
GET /api/v2/quality/rules
POST /api/v2/quality/rules
PATCH /api/v2/quality/rules/:id
DELETE /api/v2/quality/rules/:id
POST /api/v2/quality/rules/:id/enable
POST /api/v2/quality/rules/:id/disable
GET /api/v2/recordings
GET /api/v2/recordings/:id
PUT /api/v2/recordings/:id/review
GET /api/v2/users
POST /api/v2/users
PATCH /api/v2/users/:id
POST /api/v2/users/:id/reset-password
GET /api/v2/roles
POST /api/v2/roles
PATCH /api/v2/roles/:id
PUT /api/v2/roles/:id/permissions
GET /api/v2/audit-logs
GET /api/v2/audit-logs/:id
```
### 11.5 API 规范
- 列表统一 `page``page_size``sort`、筛选参数。
- 返回 `{ data, meta, request_id }`
- 错误返回稳定业务码,不把数据库错误直接返回前端。
- 新增、充值、重置密码、策略重排支持 `Idempotency-Key`
- 写请求校验 `If-Match` 或版本字段,防止覆盖其他管理员修改。
- 导出采用异步任务和短期下载地址,避免长请求占用 5 Mbps 公网带宽。
## 12. 录音迁移和质检
### 12.1 文件生命周期
```text
RTPEngine 写入 .part
-> 录音完成原子改名 .ready
-> Recording Worker 私网扫描
-> 拉取到 Server B 临时目录
-> 校验大小、SHA-256、音频时长
-> 原子移动到 /data/recordings/YYYY/MM/DD
-> 写 recordings 表
-> 删除 Server A 源文件
```
任何一步失败都不得先删除 Server A 文件。Worker 使用文件锁或 Redis 锁防止重复搬运,并对 `.part`、过期 `.ready`、孤儿文件分别处理。
### 12.2 播放
- API 先校验 `recording.play` 权限。
- Nginx `X-Accel-Redirect` 发送文件,Node.js 不直接读取整段音频到内存。
- 支持 HTTP Range,便于拖动播放。
- 操作日志记录播放人、录音 ID、时间和 IP。
- Server B 公网只有 5 Mbps 时需限制并发播放;后续可迁移至阿里云 OSS,使用短期签名 URL。
### 12.3 抽检规则
V2 字段与 Web Demo 保持一致:
- 规则名称。
- 客户。
- 抽检比例。
- 指定线路。
- 生效时间。
- 状态。
Worker 在录音入库后根据客户、线路和稳定哈希进行抽样,避免重启导致随机结果变化。
## 13. 权限与审计
### 13.1 权限模型
权限粒度采用 `resource.action`
```text
customers.view
customers.manage
vendors.view
vendors.manage
gateways.view
gateways.manage
routes.view
routes.manage
recharge.manage
cdr.view
cdr.export
recording.play
quality.manage
users.manage
roles.manage
logs.view
```
超级管理员、运营管理员、财务、质检和技术运维为内置角色。内置角色名称和关键权限禁止普通管理员修改。
### 13.2 审计要求
审计日志至少记录:
- `request_id`、操作人、用户账号、角色。
- 时间、IP、User-Agent。
- 模块、动作、对象类型、对象 ID。
- 修改前/后摘要,敏感字段脱敏。
- 成功/失败和业务错误码。
审计日志由服务端生成,前端传入的“操作人”不可信。
## 14. Dashboard 数据策略
- 实时在线通话和注册:Redis 热数据或 OpenSIPS 指标。
- 今日通话、费用、成本、毛利:MySQL 聚合表。
- 24 小时趋势:按 5 分钟或 1 小时预聚合,禁止首页扫描全量 CDR。
- 失败响应码:CDR 聚合。
- 异常网关:健康状态缓存。
- 质检待处理:recordings/quality_reviews 聚合。
创建定时聚合任务 `worker-metrics` 或复用 CDR Worker 增量写统计表。
## 15. 最小计费实现
虽然“费率与计费”页面延期,V2 仍需最小计费能力支持话单费用和余额。
V2 首期仅支持落地网关上的:
- 计费周期,单位秒,最大 60。
- 周期费率。
- 分钟费率展示值:`周期费率 * 60 / 计费周期`
计算使用 Decimal
```text
计费周期数 = ceil(通话秒数 / 计费周期)
费用 = 计费周期数 * 周期费率
```
客户侧费率在完整页面设计前可由受控配置表或迁移脚本维护,不对普通运营角色开放。所有费率变更必须留审计记录和生效时间。
## 16. 监控、日志和备份
### 16.1 基础监控
即使自研“监控告警”页面延期,也必须部署:
- Server A/B CPU、内存、磁盘、网络、负载。
- OpenSIPS 进程、SIP 请求量、响应码、活动 Dialog。
- RTPEngine 会话、丢包、端口使用率。
- tmpfs 使用率和最老录音文件年龄。
- Redis 内存、连接、Stream 长度、Pending。
- MySQL 连接、慢查询、锁等待、磁盘。
- API P95/P99、5xx、Worker 延迟。
### 16.2 日志
- 应用日志输出 JSON,包含 `request_id``call_id``event_id`
- journald 保留短期日志;可选 Loki/Promtail 后续集中化。
- 禁止在普通日志输出 SIP 密码、Web Token 和录音内容。
### 16.3 备份
- MySQL:每日全量备份 + binlog,定期恢复演练。
- RedisAOF + RDBRedis 不是业务主数据唯一副本。
- 录音:数据盘快照,后续转 OSS 生命周期管理。
- OpenSIPS、Nginx、systemd、Prometheus 配置进入 Git。
- 备份密钥与业务数据分开保存。
## 17. 开发与部署阶段
### 阶段 0:确认技术决策(2-3 天)
- 确认 SIP 公网端口、RTP 端口段和运营商 IP 范围。
- 确认预估并发、CPS、日通话量、日录音小时数。
- 确认客户余额实时控制规则和最低余额门槛。
- 确认录音保存周期和是否接 OSS。
- 确认 MySQL 还是 MariaDB。本文默认 MySQL;若沿用现有 MariaDB,需先验证 Prisma 和 SQL 兼容性。
### 阶段 1:服务器和网络(3-4 天)
- 创建 VPC、Server A/B、EIP、数据盘和安全组。
- 安装基础工具、时间同步、防火墙。
- 验证私网 RTT、带宽、磁盘吞吐和 DNS。
- 输出 `infra-check.md` 和端口验收结果。
### 阶段 2Server A 通信栈(5-7 天)
- 安装 OpenSIPS 3.6.x、RTPEngine 和内核模块。
- 配置 SIP 15060、RTP 端口范围和 MI 本地端口。
- 配置 Redis 连接、HEP、录音 tmpfs、指标和安全限制。
- 完成 IP 认证、SIP 注册认证和基础呼叫。
- 完成单通、双通、挂断、失败响应和录音测试。
### 阶段 3Server B 基础服务(4-5 天)
- 安装 Nginx、Node.js、Redis、MySQL。
- 部署 HOMER、Prometheus、Grafana 和 exporters。
- 初始化数据盘、备份目录和 TLS。
- 创建应用系统用户、systemd 服务账户和最小权限目录。
### 阶段 4:后端骨架(5 天)
- 初始化 monorepo、NestJS、Prisma、OpenAPI、日志和健康检查。
- 完成认证、JWT 刷新、RBAC、审计拦截器。
- 建立数据库迁移、种子数据和 CI。
- 完成 Redis 客户端、Stream 基础库和 outbox publisher。
### 阶段 5:运营业务 API10-12 天)
- 客户、客户网关、策略、充值。
- 供应商、落地网关、落地线路组。
- 用户、角色、权限、操作日志。
- 接入当前 React Demo,移除 Mock 数据。
### 阶段 6:通话、CDR 和录音闭环(8-10 天)
- Redis 配置发布和回滚。
- OpenSIPS 呼叫时查询和路由。
- CDR Stream、幂等消费、最小计费、余额扣减。
- Recording Worker、文件校验、Range 播放。
- HOMER Call-ID 跳转。
### 阶段 7Dashboard 和质检(5-7 天)
- Dashboard 聚合指标。
- 抽检规则、录音列表、连续播放、质检结果。
- 权限校验和播放审计。
### 阶段 8:测试和上线(7-10 天)
- 功能、集成、故障、性能、安全测试。
- 全量上线演练和回滚演练。
- 小流量客户灰度。
- 观察 24-72 小时后扩大流量。
预计总周期 8-10 周,建议至少配置:后端 1 人、SIP/媒体 1 人、前端 1 人、运维 0.5 人、测试 0.5 人。单人实施需要相应延长周期并优先保证呼叫、CDR、余额和录音闭环。
## 18. 后端代码完成标准
- 所有本期页面均使用真实 API,不再依赖硬编码 Mock。
- OpenAPI 可生成前端类型或 API Client。
- 数据库迁移可在空库一键执行,也能回滚最近一次变更。
- 关键服务有 `/health/live``/health/ready`
- CDR 重复投递不会重复扣费。
- 充值重复请求不会重复加余额。
- 配置发布失败不会留下半套 Redis 配置。
- 录音搬运失败不会删除源文件。
- RBAC 在服务端生效,隐藏按钮不能替代权限校验。
- 敏感操作全部进入审计日志。
- 单元测试覆盖金额、优先级、主被叫条件、幂等和权限。
## 19. 测试与验收
### 19.1 功能验收
- IP 和 SIP 注册两种客户网关认证成功/失败路径。
- 主叫等于、主叫前缀、被叫等于、被叫前缀及组合条件。
- 多策略优先级和多落地网关优先级。
- 网关启停、禁呼时段、编码、号码前缀转换。
- 充值、余额前后值、授信和审计。
- CDR 字段、挂断原因、客户费用、成本费用。
- 抽检规则、录音播放、上下条、自动播放、保存质检结果。
- 用户、角色、权限和重置密码。
### 19.2 性能验收
压测目标必须在阶段 0 根据业务量确认。最低应覆盖:
- 目标 CPS 的 1.5 倍持续 30 分钟。
- 目标并发的 1.2 倍持续 2 小时。
- Redis 呼叫时查询 P99。
- CDR Worker 堆积恢复速度。
- 100% 录音下 tmpfs 增长和迁移速度。
- 多用户同时查询 CDR 和播放录音时 Server B 带宽。
### 19.3 故障验收
- Redis 短暂不可用。
- MySQL 重启和事务回滚。
- CDR Worker 异常退出后重领 Pending。
- Recording Worker 重复执行。
- Server B 数据盘接近满。
- Server A tmpfs 达 70%/85%。
- RTPEngine 重启。
- 配置版本回滚。
### 19.4 安全验收
- SIP 扫描、暴力注册、非法来源 IP、超 CPS。
- 越权访问其他模块 API。
- 重放充值请求。
- SQL 注入、XSS、CSRF、弱密码和 Session 失效。
- 录音 URL 过期、Range 越权和路径穿越。
## 20. 上线和回滚
### 20.1 上线顺序
1. 备份 MySQL、Redis、OpenSIPS 和 Nginx 配置。
2. 部署 Server B 数据库迁移和后端,但暂不切流。
3. 发布 Redis 配置快照并校验。
4. 部署 Server A OpenSIPS/RTPEngine 配置。
5. 使用测试账号完成真实呼叫、CDR、扣费和录音。
6. 灰度一个低风险客户或一条线路。
7. 观察指标、CDR 差异、余额和录音完整性。
8. 逐步扩大流量。
### 20.2 回滚
- Web/API:切回上一个发布目录或镜像。
- 数据库:使用向前兼容迁移;上线窗口不执行不可逆删列。
- Redis 配置:切回上一个 `cfg:active_version`
- OpenSIPS:恢复上一版配置并执行语法检查后重载。
- RTPEngine:恢复上一版 systemd 参数。
- CDR:停止 Consumer,不删除 Stream;修复后重放。
- 录音:停止删除 Server A 源文件,保留 B 端临时文件等待人工处理。
## 21. 主要风险
| 风险 | 影响 | 控制措施 |
| --- | --- | --- |
| Server A 单点 | 重启影响通话 | V2 接受;配置快速恢复;后续增加双节点 |
| 仅开放 15060 | 无 RTP 或单通 | 必须开放并验证 RTP UDP 端口段 |
| 3 GiB tmpfs 打满 | 录音丢失、进程异常 | 高频搬运、阈值告警、降级策略 |
| Server B 5 Mbps | 多录音播放卡顿 | Range、限并发、后续 OSS/CDN |
| Redis 成为呼叫热路径单点 | 新呼叫失败 | AOF/RDB、监控、后续主从/Sentinel |
| CDR 重复消费 | 重复扣费 | Stream ACK、唯一索引、幂等事务 |
| 配置半发布 | 路由不一致 | MySQL Outbox + Redis 版本原子切换 |
| HOMER 数据膨胀 | 数据盘占满 | 独立存储、保留周期、容量告警 |
| 金额浮点误差 | 对账差异 | Decimal/整数最小单位,禁止 JS Number 计费 |
## 22. 下一步执行清单
### 立即执行
- [ ] 确认预计 CPS、并发、日话单、录音小时数。
- [ ] 确认 Server A RTP 端口段和安全组来源范围。
- [ ] 确认 Server B 数据盘容量、IOPS 和录音保留期。
- [ ] 确认 MySQL/MariaDB 最终选择。
- [ ] 申请域名和 TLS 证书。
- [ ] 创建预生产 VPC 和两台服务器。
### 服务器部署完成条件
- [ ] Server A OpenSIPS、RTPEngine、Redis 私网连接、HEP、tmpfs、exporter 正常。
- [ ] Server B Nginx、Node.js、Redis、MySQL、HOMER、Prometheus、Grafana 正常。
- [ ] 安全组端口和主机防火墙验收。
- [ ] 备份、日志轮转、时间同步和系统用户验收。
- [ ] 完成端到端测试呼叫并在 HOMER 查询到信令。
- [ ] 完成录音从 A 内存盘到 B 数据盘的搬运测试。
### 后端开发启动条件
- [ ] 创建 V2 monorepo 和 CI。
- [ ] 提交 Prisma Schema V1 和初始化迁移。
- [ ] 提交 OpenAPI V2 基线。
- [ ] 提交 Redis Key 和 Stream 契约。
- [ ] 完成 Auth/RBAC/Audit 骨架。
- [ ] 先实现客户、网关、线路组和配置发布,再实现 CDR/录音。
## 23. V2 交付物
```text
1. 阿里云双机部署记录和安全组清单
2. Server A/Server B 幂等安装脚本
3. OpenSIPS 和 RTPEngine 配置仓库
4. MySQL DDL/Prisma migrations
5. Redis Key/Stream 契约
6. OpenAPI 文档
7. LisgloSIPS API 和 Workers 源码
8. React Web 与真实 API 集成版本
9. Prometheus/Grafana/HOMER 部署配置
10. 自动化测试、压测报告和安全检查报告
11. 上线、回滚、备份恢复和故障处置 Runbook
```
## 24. 最终实施原则
- 先打通“配置发布 -> 呼叫 -> CDR -> 计费 -> 余额 -> 录音 -> 质检”闭环,再扩展页面。
- MySQL 是业务事实源,Redis 是呼叫热路径和队列,不反向替代主库。
- 金额、CDR、充值和录音全部按可重试、可审计、幂等设计。
- Server A 保持精简,Server B 承担业务复杂度。
- 待设计菜单不阻塞底层必需能力,但不得在 V2 临时扩张为未确认的产品页面。
- 所有部署和配置必须脚本化、版本化、可回滚。
## 25. Codex 全流程分步实施规范
### 25.1 目的
本项目计划主要由 Codex 持续实施。为避免单次任务过大、上下文丢失、误操作和新会话重复工作,整个 V2 被拆分为 31 个原子任务 `S00-S30`。每个会话默认只完成一个任务,除非用户明确授权合并。
详细实时状态记录在 `IMPLEMENTATION_STATUS.md`,服务器密码只保存在被 `.gitignore` 排除的 `.codex-private/SERVER_ACCESS.md`
### 25.2 服务器固定代号
| 代号 | IP | SSH 用户 | 职责 |
| --- | --- | --- | --- |
| Server A | `100.90.90.90` | `hector` | 核心通信网关 |
| Server B | `100.90.90.91` | `hector` | 业务、缓存与监控中心 |
| Server T | `100.93.185.30` | `hector` | 已安装 OpenSIPS,模拟 SIP 客户 |
主文档不保存明文密码。新会话只有在用户授权当前任务连接服务器后,才读取私密访问文件,并禁止在回复或日志中回显密码。
### 25.3 单会话执行纪律
1. 先读取 `SOFTSWITCH_PLATFORM_DESIGN_V2.md``IMPLEMENTATION_STATUS.md`
2. 只执行状态文件中的“当前任务”。
3. 服务器任务先做只读检查,再做写操作。
4. 修改配置前备份,记录绝对路径、时间戳和回滚命令。
5. 服务重启、防火墙、SSH、数据库迁移、数据删除前说明影响。
6. 不能因安装困难随意更换技术栈或扩大公网端口。
7. 安装软件必须记录来源、版本和校验结果。
8. 完成后执行该任务的验收,不以“命令无报错”代替功能验证。
9. 更新状态文件后再结束会话。
10. 出现阻塞时停止在安全状态,不绕过安全控制继续后续任务。
### 25.4 原子任务顺序
#### S00-S02:安全接入和资产盘点
**S00 SSH 安全接入与凭据迁移**
- 用户明确授权后连接 A/B/T。
- 在本机生成项目专用 Ed25519 Key,不覆盖现有 Key。
- 安装公钥并验证三个独立新会话。
- 备份 `sshd_config`,确认不会锁死后只调整 SSH 管理端口,不禁止 SSH 密钥登录。
- 轮换当前密码,私密文件同步更新或改为只记录 Key。
- 验收:新端口 Key 登录成功;sudo 可用;现有会话保留到新会话验证完成。
**S01 三机只读资产盘点**
- 只读取 OS、内核、CPU、内存、磁盘、网卡、路由、时间、端口、服务、包版本。
- 盘点 T 上现有 OpenSIPS 配置和测试能力,不修改。
- 输出 `docs/inventory-A.md``inventory-B.md``inventory-T.md`
- 验收:报告包含差异、风险和安装前备份清单。
**S02 网络与安全组验收**
- 验证 A/B/T 双向可达性和实际私网 RTT。
- 确认 A 的 SIP 15060/UDP、RTP 端口段;B 的 443/TCP、私网 HEP 9060/UDP。
- 不通过 SSH 直接修改云安全组;生成阿里云控制台操作清单供用户确认。
- 验收:形成端口矩阵和最小暴露方案。
#### S03-S06Server B 基础设施
**S03 Server B 基础系统初始化**
- 更新系统、安装基础包、chrony、nftables、fail2ban。
- 检查并初始化数据盘目录,不格式化未知数据盘。
- 创建应用、录音和监控系统用户与目录权限。
- 验收:重启后时间、挂载、权限和防火墙正常。
**S04 MySQL 与 Redis**
- 固定版本安装并限制监听地址。
- 创建业务库/用户,启用 MySQL binlog、Redis AOF/RDB。
- 配置备份脚本和健康检查。
- 验收:重启恢复、权限隔离、备份和恢复样例通过。
**S05 Node.js、Nginx 与 TLS 基线**
- 安装固定 Node.js LTS 和 pnpm。
- 建立 `/opt/lisglosips/releases``current` 和 systemd 模板。
- 配置 Nginx 静态站点、API 代理、Range、限流和 HTTPS 预案。
- 验收:占位健康页通过 Nginx 访问,应用端口不暴露公网。
**S06 HOMER 与基础监控**
- 部署 Heplify-server/HOMER、Prometheus、Grafana、exporters。
- HOMER 数据和业务 MySQL 分离。
- 验收:B 私网 9060 接收测试 HEP;A/B 主机指标可见。
#### S07-S10:后端基础
**S07 后端 Monorepo 骨架**
- 创建 NestJS/Fastify、Prisma、结构化日志、配置校验、健康检查和测试框架。
- 建立 CI 命令:lint、typecheck、test、build。
- 验收:本地和 B 均可构建;健康检查可用。
**S08 数据库 Schema 与迁移**
- 实现本文第 10 章表结构、索引、Decimal、审计列和 Outbox。
- 提供空库迁移、种子和回滚/恢复说明。
- 验收:空库一键初始化;重复执行安全;Schema 测试通过。
**S09 登录认证基础**
- 登录、刷新、退出、Argon2id、失败锁定、Token/Cookie 安全。
- 验收:成功、失败、过期、刷新、注销和暴力尝试路径通过。
**S10 用户、角色、权限与审计**
- 实现用户、角色、权限矩阵、内置角色保护和重置密码。
- 服务端 RBAC 和审计拦截器必须先于业务 API 完成。
- 验收:越权被拒绝;敏感字段脱敏;审计详情可查询。
#### S11-S17:运营业务 API
**S11 客户管理**:客户 CRUD、启停、余额/授信读取。
**S12 充值流水**:客户/供应商充值、事务、幂等、并发。
**S13 客户网关**:IP/SIP 注册认证、启停、密码安全。
**S14 网关策略与配置发布**:主被叫条件、优先级、Outbox、Redis 版本。
**S15 供应商管理**:供应商 CRUD、余额、授信。
**S16 落地网关**:认证、CPS、并发、禁呼、编码、号码转换、周期费率。
**S17 落地线路组**:成员增删、优先级、并发汇总、引用校验。
每个任务必须同时完成:数据库迁移、领域校验、API、权限、审计、单元测试、集成测试和 OpenAPI,不允许先堆积未测试接口后统一补测。
#### S18-S21:通信栈和测试客户
**S18 Server A OpenSIPS 基线**
- 先备份现状,再安装/固定 OpenSIPS 3.6.x。
- 配置 15060、认证、防扫描、MI 本地绑定和模块基线。
- 每次改动先执行语法检查,验证失败立即回滚。
**S19 RTPEngine 与录音 tmpfs**
- 配置内核转发、RTP 端口段、3 GiB tmpfs 和 `.part -> .ready` 生命周期。
- 验收:双向音频、NAT、录音、重启和 tmpfs 阈值。
**S20 Redis 热路径、HEP 与指标**
- 实现 Lua 原子检查、版本化配置读取、CDR XADD、HEP 发送和指标采集。
- 验收:Redis 超时、配置缺失、HEP 中断均有明确降级和日志。
**S21 Server T 客户模拟**
- 在不破坏 T 现有 OpenSIPS 的前提下配置测试域、IP 认证和 SIP 注册账号。
- 编写成功呼叫、忙、拒绝、超时、错误密码、超 CPS 测试脚本。
- 验收:所有脚本可重复执行并输出 Call-ID。
#### S22-S26:异步闭环
**S22 CDR Redis Stream**:事件契约、Consumer Group、ACK、Pending、死信、幂等。
**S23 最小计费**:Decimal 周期计费、客户费用、成本、余额扣减。
**S24 录音搬运与播放**:私网拉取、哈希、原子移动、Range、权限和源文件删除保护。
**S25 质检后端**:规则、稳定抽样、连续录音查询、质检保存和审计。
**S26 Dashboard 聚合**:实时指标和预聚合趋势,避免扫描全量 CDR。
#### S27-S30:联调和上线
**S27 React 接入真实 API**
- 按菜单逐页替换 Mock;每次只接入一个领域。
- 增加加载、空状态、错误、权限隐藏和并发更新提示。
- 待设计菜单不接临时 API;S30 后二期变更要求导航中隐藏这些入口。
**S28 三机端到端联调**
- T 发起 SIP -> A 鉴权/路由/媒体/录音 -> B CDR/费用/信令/质检。
- 按 Call-ID 核对 OpenSIPS、Redis Stream、MySQL、HOMER 和录音文件。
**S29 性能、故障与安全测试**
- 执行 CPS、并发、录音、Redis/MySQL/Worker 故障和 Web/SIP 安全测试。
- 所有失败必须形成缺陷、修复和复测记录。
**S30 备份、Runbook、灰度与上线**
- 完成恢复演练、上线/回滚脚本和灰度。
- 上线前冻结版本和数据库迁移,输出最终验收报告。
### 25.5 S30 后二期需求变更
S30 完成本地 KVM A/B/T 闭环后,用户基于实际试用继续提出二期变更。二期变更不重做 S00-S30 基线,按“在既有架构上最小增量、可验证、可回滚”的原则落地。当前本地冻结基线仍为 `s28-v2-20260621220924`B 当前运行 release 为 `s33-active-calls-20260623094000`,多次二期前端/API/通信配置小改覆盖在该 release 上。阿里云迁移仍不得自动开始,迁移前必须按 S30 Runbook 重新演练。
#### 25.5.1 登录与验证码
- 登录页必须使用后端图形验证码,验证码刷新失败时应有明确错误提示,登录失败后必须刷新验证码。
- 登录防爆破、Session/Refresh Token、Cookie 安全配置仍归属 S09/S10 安全基线。
- 禁止在文档、日志或页面中输出明文密码;初始账号、服务器凭据只允许通过私密目录和受控流程读取。
#### 25.5.2 当前通话与强制挂断
新增“当前通话”能力,定位为实时运维视图,不作为 CDR 或计费真相源。
- Web 在“业务”菜单下提供“当前通话”页面,展示 OpenSIPS 当前 Dialog 列表。
- B API 提供:
- `GET /api/v2/active-calls`
- `POST /api/v2/active-calls/:id/hangup`
- 权限新增:
- `active_calls.view`
- `active_calls.manage`
- 超级管理员具备查看和挂断权限;技术运维具备查看和挂断权限;运营管理员默认仅查看。
- API 通过 B 上的受限 SSH key 调用 A 上的 forced-command `/usr/local/sbin/lisglosips-call-control`,只允许 `dlg_list``dlg_end_dlg`。OpenSIPS MI HTTP 仍只绑定 `127.0.0.1:8888`
- 当前通话列表字段至少包括:
- Dialog ID / Call-ID
- 主叫、被叫
- 呼叫方 IP
- 落地 IP
- 状态、开始时间、持续时长
- 强制挂断操作
- 呼叫方 IP 和落地 IP 的解析优先级为 SIP Contact、SDP `c=IN IP4/IP6`、SIP URI、bind 地址兜底。生产上若落地网关不提供 Contact 或 SDP 连接地址,落地 IP 可能退回为 OpenSIPS 本地地址;后续可在路由侧写入 Dialog 变量提高精度。
- 强制挂断必须弹出自研确认弹窗;操作必须进入 RBAC 和审计。
- OpenSIPS `dlg_end_dlg` 可能返回 `500 Operation failed`,但 Dialog 进入 state `5` 并随后清空时,API 可按“已受理/处理中”处理。
- 当前通话页面默认支持 5 秒自动刷新,并保留手动刷新。
#### 25.5.3 列表计数、删除和启停确认
二期增加删除能力时,优先使用软删除和引用保护,不允许破坏热路径配置一致性。
- 客户管理:
- 列表显示客户网关数。
- 删除客户前必须确认。
- 若客户关联客户网关数大于 0,则拒绝删除。
- 客户网关管理:
- 支持删除,删除前必须确认。
- 删除客户网关时,其关联路由策略应同步停止生效或软删除,并发布配置 Outbox。
- 启用/禁用前必须确认。
- 供应商管理:
- 列表显示落地网关数。
- 删除供应商前必须确认。
- 若供应商关联落地网关数大于 0,则拒绝删除。
- 落地网关管理:
- 支持删除,删除前必须确认。
- 启用/禁用前必须确认。
- 若落地网关仍被未删除落地线路组引用,默认拒绝删除,避免线路组热配置引用已删除网关。若未来要支持自动从线路组移除,需单独设计成员优先级重排和配置发布语义。
- 落地线路组:
- 列表显示正在使用该线路组的去重客户网关数。
- 删除线路组前必须确认。
- 若有客户网关策略正在使用该线路组,则拒绝删除。
- 所有删除按钮使用红色警示态;所有确认弹窗使用自研 UI,不再使用浏览器原生 `window.confirm`
#### 25.5.4 充值记录与计费流水语义
充值记录页面只展示人工充值,不承载话单消费扣费记录。
- 页面人工充值写入 `customer_recharges` / `vendor_recharges` 或等价充值流水。
- CDR Worker 的话单消费扣费只写 raw/rated CDR 和余额扣减结果,不再写客户充值记录。
- 充值记录 API 需要过滤历史由 `worker-cdr``cdr:``CDR_CHARGE:` 等来源产生的消费类流水,避免在“充值记录”中混入话单扣费。
- Web 充值成功后只局部更新对应余额和充值记录列表,不应触发整页 `refreshApi()`,避免供应商充值后长时间停留在“拉取 API”。
- 话单消费、客户费用、供应商成本仍以 CDR/rated CDR 和余额变更为准。
#### 25.5.5 动态路由、CDR 和配置发布补齐
完整新建链路测试暴露出 S28 早期热路径仍有硬编码成功 CDR 和落地网关的问题。二期要求热路径使用配置发布结果动态选择路由。
- OpenSIPS Lua 热路径按客户网关策略匹配线路组,并选择线路组内首个启用落地网关。
- OpenSIPS 成功 CDR 从 Dialog 变量写入动态 customer/gateway/policy/vendorGateway/lineGroup,不再写硬编码对象。
- B 必须提供 `config-publisher.env` 并保持 `lisglosips@config-publisher` active。
- CDR Worker 对 `config_version` 等大数版本字段必须避免 MySQL INT 越界。
- 端到端验收流程应覆盖:
1. 新建供应商
2. 新建落地网关
3. 新建落地线路组
4. 新建客户
5. 新建客户网关
6. 配置客户网关策略指向新线路组
7. 给客户充值
8. 给供应商充值
9. 打通电话
10. 当前通话页面查看并强制挂断
11. 核对 raw/rated CDR、客户余额、供应商成本和审计记录
#### 25.5.6 前端信息架构和交互细化
二期 UI 调整遵循当前控制台风格,不引入新的视觉体系。
- “待设计”菜单在导航中隐藏,包括费率与计费、SIP 运维、监控告警、系统设置。底层最小计费、HOMER、Prometheus/Grafana、配置文件能力仍按 S00-S30 保留。
- 客户管理、客户网关管理、充值记录、供应商管理、落地网关管理等页面中,ID 和名称类列宽应适当收窄,长文本使用省略号,避免挤占金额、状态和操作列。
- 话单详情抽屉应分区展示:
- 顶部主叫到被叫概览
- 通话摘要:时长、挂断原因、客户费用、成本费用
- 链路信息:客户网关、呼叫 IP、落地网关、线路 IP
- 时间轴:呼叫、接通、结束时间
- 录音播放和信令查看入口
- 当前通话、删除、启停、重置密码、策略删除、质检规则删除等确认场景统一使用自研确认弹窗。
#### 25.5.7 10 路虚拟呼叫测试工具
为观察当前通话页面的实时变化,二期新增 T 侧虚拟呼叫编排工具。
- 工具路径:`/opt/lisglosips-s40/lisglosips-s40-virtual-calls.py`,源码纳入 `infra/server-t/s40/`
- 默认测试参数:
- 总计 10 路虚拟呼叫。
- 每隔 15 秒发起 1 路。
- 前 5 路先返回 `180 Ringing`45 秒后返回 `200 OK`,通话保持 600 秒后 BYE。
- 后 5 路先返回 `180 Ringing`70 秒后返回 `480 Temporarily Unavailable`
- 使用当前本地测试策略的主叫 `s36-1001` 和被叫 `13800136036`,按 Call-ID 序号区分接通/不接通场景。
- T 上旧 S28 UAS 占用 `100.93.185.30:50620`,完整测试时需要临时停旧 UAS,由 S40 UAS 接管;测试结束或提前停止后必须恢复旧 S28 UAS。
- 首次测试暴露 A `fr_inv_timeout=30` 会在 45 秒接通前返回 `408 Request Timeout`。为了支持 45 秒接通和 70 秒未接场景,本地 KVM A 已将 `/etc/opensips/opensips.cfg``modparam("tm", "fr_inv_timeout", 30)` 临时调整为 `95` 并重启 OpenSIPS。该调整属于测试窗口配置,迁移生产前必须重新评估运营侧真实振铃超时策略。
- 停止完整测试命令:
```bash
sudo pkill -f /opt/lisglosips-s40/lisglosips-s40-virtual-calls.py
```
- 若 T 旧 UAS 未恢复,可执行:
```bash
sudo nohup runuser -u nobody -- /usr/bin/python3 /opt/lisglosips-s28/lisglosips-s28-sip.py uas --host 100.93.185.30 --port 50620 >/tmp/lisglosips-s28-uas.log 2>&1 &
```
#### 25.5.8 二期变更的回归要求
每次二期变更完成后至少执行:
- 相关单元测试或 e2e 测试。
- `corepack pnpm@10.33.0 typecheck`
- `corepack pnpm@10.33.0 lint`
- `corepack pnpm@10.33.0 build`
- 若涉及 A/B/T 三机链路,则执行端到端验收并按 Call-ID 记录结果。
- 若涉及 OpenSIPS 配置,必须先备份、执行 `opensips -C -f /etc/opensips/opensips.cfg`,再重启或 reload,并记录回滚点。
- 若涉及 Web 发布,必须备份 B 当前 release 的 `apps/web/dist`,覆盖后执行 `nginx -t` 和 reload。
- 每次完成后更新 `IMPLEMENTATION_STATUS.md`,写明修改内容、验证结果、回滚方式和遗留问题。
#### 25.5.9 号码库、归属地运营商识别与屏蔽地区路由
新增“号码库”菜单,定位为呼叫路由和话单归属地的基础数据管理,不属于普通费率页面。菜单包含四个 Tab:
- 地级市字典:维护稳定的地级市编码、省份、地级市名称、状态和生效期,供手机号码库、城市区号、落地网关屏蔽地区和话单快照统一引用。
- 手机号码库:通过手机号前 7 位号段匹配地级市,预计约 80 万条号段。
- 城市区号:保存全国固话区号,例如 `0551` 对应合肥、`021` 对应上海,维度到地级市。
- 运营商号码段规则:通过手机号前 3-4 位匹配归属运营商,用于快速判断中国移动/中国联通/中国电信/广电/虚拟运营商等。
是否增加地级市字典:需要增加。原因是手机号码库和城市区号都要统一落到地级市维度,落地网关屏蔽地区也需要引用稳定地区编码;如果只在号段表中保存城市文本,会造成同名、改名、直辖市、省市归属和历史变更难以维护。建议新增 `geo_cities` 字典,字段至少包括:
- `code`:行政区划码或项目稳定编码,作为主键或唯一键。
- `province_code``province_name`
- `city_code``city_name`
- `city_level`:地级市、直辖市、地区、自治州等。
- `status``effective_from``effective_to`
- 审计列和软删除列。
号码库数据模型建议:
- `phone_number_segments`
- `segment7`:手机号前 7 位,唯一。
- `city_code`:关联 `geo_cities`
- `province_name``city_name` 可冗余快照,便于导入校验和快速展示。
- `carrier` 可选;若和运营商规则冲突,运营商以 `carrier_prefix_rules` 为准,并记录数据质量告警。
- `source``batch_id``effective_from``effective_to``updated_at`
- `carrier_prefix_rules`
- `prefix`:手机号前 3-4 位,唯一或按生效期唯一。
- `carrier``MOBILE``UNICOM``TELECOM``BROADCAST``MVNO``UNKNOWN` 等枚举。
- `priority`:前缀重叠时按最长前缀和优先级匹配。
- `effective_from``effective_to`
- `phone_area_codes`
- `area_code`:固话区号,例如 `021``0551`
- `city_code`:关联 `geo_cities`
- `province_name``city_name` 展示冗余。
- `vendor_gateway_blocked_regions`
- `vendor_gateway_id`
- `city_code`,必要时支持省级屏蔽可通过 `region_scope=PROVINCE/CITY` 建模。
- `created_at``created_by`
匹配规则:
- 手机号归属地:优先取规范化后的被叫号码前 7 位匹配 `phone_number_segments.segment7`
- 手机号运营商:按被叫号码前 4 位、前 3 位依次匹配 `carrier_prefix_rules`,优先最长前缀。
- 固话归属地:对被叫号码做号码规范化后匹配城市区号;区号需要支持 `0xx``0xxx`,并注意去掉外呼前缀、国家码 `+86/0086` 后再判断。
- 无法识别时,城市和运营商写 `UNKNOWN`,呼叫不应仅因号码库缺失被拒绝,除非客户或全局策略明确要求。
每通话单必须保存号码识别快照:
- `callee_city_code`
- `callee_city_name`
- `callee_province_name`
- `callee_operator`
- 可选保存 `callee_number_type``MOBILE``LANDLINE``INTERNATIONAL``UNKNOWN`
落地网关屏蔽地区路由要求:
- Config Publisher 发布线路组时,需要把线路组成员落地网关的屏蔽地区一起写入 Redis 热路径配置。
- OpenSIPS/Lua 在选中客户网关策略和线路组后,先识别被叫地级市,再按线路组成员优先级选择落地网关。
- 若某落地网关屏蔽该地级市或其所在省份,则跳过该网关,继续尝试同一线路组中的下一落地网关。
- 若同一线路组所有可用落地网关都被屏蔽或不可用,返回明确失败原因,例如 `NO_VENDOR_ROUTE_REGION_BLOCKED`,并写入失败 CDR。
- 成功 CDR 需要写入最终实际选中的 `vendor_gateway_id` 和号码识别快照;不能只记录第一次被跳过的网关。
- 当前 V2 单节点 OpenSIPS 热路径必须保持可预测和低延迟,80 万手机号段不应在每通电话中扫描 MySQL。推荐路径是 B 侧导入 MySQL 后,由 Config Publisher 生成 Redis 查找结构:
- `cfg:v:{version}:phone_segment:{segment7}` -> city/operator 快照。
- `cfg:v:{version}:area_code:{areaCode}` -> city 快照。
- `cfg:v:{version}:carrier_prefix:{prefix}` -> carrier。
- `cfg:v:{version}:vendor_gateway:{id}:blocked_regions` -> city/province set 或紧凑 JSON。
实施拆分建议:
1. 数据建模与迁移:新增地级市字典、手机号码库、城市区号、运营商前缀规则、落地网关屏蔽地区和 raw CDR 号码识别字段。此步骤涉及数据库 schema migration,执行前必须备份 B MySQL,说明回滚点;80 万号段导入需要单独评估索引和迁移时间。
2. 导入与管理 API:实现号码库导入、分页查询、按号段/区号/城市检索、批次校验、重复号段冲突报告;大批量导入应走文件/后台任务,不建议通过普通 JSON 表单一次提交。
3. Web 菜单:新增“号码库”菜单和四个 Tab(地级市字典、手机号码库、城市区号、运营商号码段规则),先支持查询、导入结果查看和基础维护;超大号段列表必须服务端分页和筛选,不做前端全量加载。
4. Config Publisher 与 Redis 热路径:把号码库快照、运营商规则、城市区号和落地网关屏蔽地区发布到版本化 Redis key;保留上一版本用于一键回滚。
5. OpenSIPS/Lua 路由:在当前动态路由基础上增加号码规范化、地级市/运营商解析、屏蔽地区跳过下一落地网关、失败原因写入;改动前必须备份 A `/etc/opensips/opensips.cfg` 和 Lua,执行 `opensips -C -f` 后再重启。
6. CDR Stream 与 Worker:升级 CDR event schema,解析并入库归属地/运营商字段;保持向后兼容旧 schema,避免旧 Stream 或 pending 消息死信。
7. 话单中心展示:列表/详情增加地级市、运营商展示和筛选;历史无字段话单显示 `UNKNOWN``-`
8. 端到端验收:构造同一线路组两个落地网关,其中第一个屏蔽目标地级市,第二个允许;发起测试呼叫,验证当前通话/成功 CDR 使用第二个网关,并保存正确地级市和运营商;再验证全部网关屏蔽时失败 CDR 原因为 `NO_VENDOR_ROUTE_REGION_BLOCKED`
风险与约束:
- 80 万手机号段属于大批量主数据,导入、索引、Redis 发布和回滚都需要独立 Runbook,不能夹在普通前端小改中发布。
- 手机号段和行政区划会变更,必须保留数据来源、批次和生效期,避免“更新号码库”改变历史 CDR 解释。
- Redis 热路径内 JSON 字符串解析能力有限,复杂匹配逻辑应提前在 Config Publisher 生成适合 Lua 快速读取的结构。
- 地区屏蔽属于路由策略,必须与并发/CPS、禁呼时段、编码限制的优先级关系固定下来:推荐先过滤状态/禁呼/地区屏蔽,再做并发/CPS 占用。
本地实施状态:
- 2026-06-24 已完成第 1、2 步本地代码基线:Prisma Schema、迁移文件、号码库 API、RBAC/seed、审计和后端测试已完成;尚未发布到 B,尚未执行 MySQL migration,尚未导入真实 80 万号段。
- 2026-06-24 已完成第 3 步 Web 本地代码基线:运营菜单新增“号码库”,页面包含地级市字典、手机号码库、城市区号、运营商号码段规则四个 Tab;每个 Tab 支持服务端分页查询入口、筛选、刷新和批量 JSON 导入弹窗。该导入弹窗只适合小批校验和后台接口联调,真实 80 万号段仍需按后续导入任务/Runbook 走文件或后台任务。
- 2026-06-24 已完成第 4、5 步本地代码基线:Config Publisher 会把地级市、手机 7 位号段、固话区号、运营商前缀和落地网关屏蔽地区发布为版本化 Redis key;号码库导入会写入 `number_library_config` outbox 触发新快照。OpenSIPS S28 Lua 热路径会解析被叫号码,命中屏蔽城市/省份时跳过当前落地网关并尝试同一线路组下一网关,全部被屏蔽时返回 `NO_VENDOR_ROUTE_REGION_BLOCKED`;成功/失败 CDR Stream 已携带归属地和运营商字段。当前仅完成本地文件,尚未发布 B/A;发布 A 前必须重新加载 Lua、替换 `/etc/opensips/opensips.cfg` 前备份,并在 A 上执行 `opensips -C -f /etc/opensips/opensips.cfg` 后再重启。
- 2026-06-24 已完成第 6、7 步本地代码基线:CDR Stream 解析兼容新增 `callee_city_code``callee_city_name``callee_province_name``callee_operator``callee_number_type`,旧 Stream 消息缺字段时仍可处理;CDR Worker 会把这些字段写入 `raw_cdrs` 号码识别快照列。新增 `GET /api/v2/cdrs``GET /api/v2/cdrs/:id`,复用 `cdr.view` 权限,支持主叫、被叫、客户网关、落地网关、地级市代码和运营商筛选。Web 话单中心已接入真实 CDR API,列表和详情展示地级市、运营商、号码类型、SIP 状态码和费用字段。
第 1、2 步已完成本地代码基线:
- Prisma Schema 和迁移新增:
- `geo_cities`
- `phone_number_segments`
- `phone_area_codes`
- `carrier_prefix_rules`
- `vendor_gateway_blocked_regions`
- `raw_cdrs` 被叫地级市、省份、运营商、号码类型快照字段
- API 模块新增 `NumberLibraryModule`
- `GET /api/v2/number-library/cities`
- `POST /api/v2/number-library/cities/import`
- `GET /api/v2/number-library/phone-segments`
- `POST /api/v2/number-library/phone-segments/import`
- `GET /api/v2/number-library/area-codes`
- `POST /api/v2/number-library/area-codes/import`
- `GET /api/v2/number-library/carrier-prefix-rules`
- `POST /api/v2/number-library/carrier-prefix-rules/import`
- 权限新增:
- `number_library.view`
- `number_library.manage`
- 当前导入接口采用单批 `items` upsert,单次限制 1000 条,适合脚本/后台任务分批调用。80 万号段的真实文件导入、进度表、失败明细和 Redis 发布仍属于后续步骤,不在本次第 1、2 步中直接执行。
### 25.6 Codex 新会话启动模板
```text
请先读取 SOFTSWITCH_PLATFORM_DESIGN_V2.md 和 IMPLEMENTATION_STATUS.md
只执行当前任务 Sxx,不提前执行下一任务。
如任务需要服务器访问,读取 .codex-private/SERVER_ACCESS.md,但不要在回复、日志或文档中回显密码。
所有服务器写操作先备份并给出回滚点;涉及重启、SSH、防火墙、数据库迁移或删除时先说明影响。
完成后执行验收并更新 IMPLEMENTATION_STATUS.md,包括状态、产物、验证、回滚、遗留问题和下一任务。
```