43 KiB
主叫号码实时分析设计核对与整改方案
日期:2026-08-31
审查版本:V1.0
被审文档:主叫号码接通率与应答率实时分析设计方案 V1.1
初始源码基线:116e316eca5b564f33ec3558dbd00201da173ed8;收尾补核对提交 d78616561ead24e54734c8dfc9b34178bfd24ab1 及当时可见的并行未提交差异。
结论:业务指标定义基本成立,可保留;实现已有基础,但证据可信度、乱序校正、采集完整性、运维隔离和验收闭环仍需整改,暂不具备认定“完整实时分析已验收”的依据。
1. 核查范围与结论边界
本次核对设计内部一致性,以及设计与本地 OpenSIPS 采集脚本、分析 Worker、状态计算、MySQL 聚合、API、React 页面、迁移、发布脚本和测试脚本的对应关系。补充查阅 SIP、OpenSIPS、Redis 官方资料,并运行现有专项单元测试和不连接外部服务的内存复现。
本次没有连接 B/T 或生产服务,没有查询线上数据库,没有执行迁移、发布、服务重启、真实呼叫或计费操作;不把历史实施记录当成本轮现场验证。原文第12节记录的 B 现场事实仍属历史记录。本地 infra/server-a/s28/opensips.cfg 是采集脚本依赖的路由布局参照,不等于当前 B 生效配置。
核查开始时已有以下其他任务文件,本次不改动、不纳入交付:docs/CODE_QUALITY_REVIEW_20260831.md、tests/reports/code-quality-20260831-dependency-audit.json、tests/reports/code-quality-20260831-repro.mjs。
**并行更新说明:**核查期间另一个任务提交了 d786165,并继续修改存储/真实测试及新增 scripts/rebuild-caller-analytics.mjs。本文已补核对:gap写入游标文件、部分回滚恢复、日志匹配与趋势时间序列化已有修复代码;分钟桶JSON键修复、SQL对账与重复消费测试为收尾时可见的候选变化。下文注明相应进展,不将其全部视为未开发,也不代替另一任务宣称已部署/验收。本轮不覆盖或提交这些变化。
证据分级:
| 标记 | 含义 | 本文如何使用 |
|---|---|---|
| 已复现 | 本轮直接执行本地纯计算,得到输出 | 确认状态计算缺陷,不等于线上事故 |
| 源码确认 | 当前文件可直接确认行为或缺少必要路径 | 能确认实现差距,发生概率、线上影响仍需联调 |
| 待现场验证 | 涉及生效配置、SIP 状态、资源与部署结果 | 作为实施前置门槛,不写成已发生事实 |
整改分为13组,其中10组P1、3组P2。P1指完整功能验收或正式启用前应关闭的问题;P2指应补齐的契约、展示和容量工程项。未凭本地审查认定已有P0线上事故。
2. 应保留的设计,以及需要澄清的约束
以下内容不应在整改时改回旧口径:
- 接通 C 以匹配的初始 INVITE 下游180/183、直接有效2xx或可信正时长证据判定;183不代表真实振铃,C不等于真人接听。
- 应答 A 以可信业务通话时长严格大于0判定;200本身、answeredAt、计费取整秒数均不能替代。不能为整改方便把A重新定义成2xx数量,也不额外要求真人检测或RTP分析。
- 原始主叫按业务呼叫去重,落地主叫按实际出局尝试计数;不同客户同号不能合并归属。
- 保留
N=F+P、T=C+F+P+U、0≤A≤C≤T,以及三个率C/T、A/T、A/C;分母0返回null。 - 后续进展回写发起时间桶;最终失败不撤销已有接通证据;历史缺少过程时不能用最终486/487倒推F。
- 统计不改变计费、余额、路由,不自动停号换路。保留独立消费者、数据库事务去重及提交后ACK。
原文关于100/180/183及200与请求方法相关的说明与 RFC 3261 §21.1–21.2 一致;“180/183算业务接通”仍是本平台产品定义,不是协议规定的接听率。
需新增两个独立概念:
- 呼叫分类未知 U:已知存在的呼叫中,不能确认是否达到C的数量;继续参与上述守恒式。
- 数据覆盖缺口 / 应答证据不完整:可能整个呼叫都未采到,或C已知但A未知。不能硬塞进U,也不能因U=0就认为所有指标完整。前者连T的真实值都可能未知,必须单独标注窗口覆盖状态。
原文“对账后P=U=0”只能作为完整采集且可修复的验收目标;不可修复的历史缺口不能为了闭环强行归零。
3. 分级问题与整改要求
R01|P1|“成功响应至结束的时间差”尚未充分证明实际通话时长
**对应原文:**第3.4、6、12节。证据:源码确认,纯计算复现;协议边界待真实验证。
packages/database/src/caller-analytics-model.ts:79–84在没有显式talkMs时,用结束时间减acceptedAt生成最终时长。apps/worker-cdr/src/analytics-main.ts:60–71只按Call-ID匹配MI,状态3或4均可生成暂定正时长。scripts/instrument-caller-analytics.mjs:31–38将挂断、超时和事务失败统一标为verified-dialog-final,该字符串本身并不能证明会话时长已核验。tests/api/caller-analytics-sip.py的INVITE/响应均为Content-Length: 0,未提供SDP offer/answer。这些场景能验证SIP响应和对话流程,但不足以独立证明实际业务会话的计时口径。
OpenSIPS 3.4官方文档区分状态3(已收到最终响应,尚未收到ACK)与4(已收到ACK);对话lifetime由对话确认起计算。不能只凭字段名或状态范围声明为“实际通话时长”。参见 OpenSIPS Dialog状态与lifetime。实际部署版本仍需现场核对。
**整改:**建立经过验证的时长来源适配层,分别保存 inviteAcceptedAt、talkStartedAt、talkEndedAt、talkDurationMs、durationEvidenceId、durationStatus 和证据版本。ACK、协商及对话状态用于核验来源,不改动最终“可信正时长>0”的业务条件。信令观察差值在核验完成前只能作为观察值,不能自动升级为权威最终时长。无ACK、协商失败、取消竞态、对话超时须逐类定义时长有效性;不确定时保留C、标记A证据待核实。使用一致时钟域;秒/微秒读取应为同一次时间采样,并检测时钟跳变,而非仅用Math.max掩盖负时长。
**关闭标准:**CRA-005/006/007/013/014及新增“200无ACK”“协商失败”“早期媒体”“零时长终态”真实验证;证明正常500ms计A、可信零时长不计A。必须保留会话开始/结束证据及来源解释。
R02|P1|结束事件错误地清除未知,采集丢失可能被统计成失败
**对应原文:**第3.3、4、7节。证据:已复现。
caller-analytics-model.ts:68–78 初始可把缺过程的leg设为unknown,但遇到 verified-dialog-final 或 platform-final 的END就清零。当前END记录不携带完整的历史阶段快照,不能证明之前没有180/183。
本轮输入只有 END(487, verified-dialog-final),得到 T=1,C=0,F=1,U=0;输入 ATTEMPT→UNKNOWN→END 也得到F=1。对缺少过程证据的下游呼叫,预期应保留U,除非补回过程或有可信完整终态快照。
另外,legCounts() 的unknown受 !connected 限制:一旦C成立,A时长丢失不会通过U反映;这不是修改守恒式的理由,而是需要独立的时长完整性字段。
**整改:**分离 ended、connectionEvidenceComplete、durationEvidenceComplete、captureGapIds。END只证明结束,不能默认证明全过程完整。平台本地明确拒绝可通过独立的“未出局终态证据”判F;下游失败只有在阶段覆盖完整时判F。终态快照应包含已观测180/183/2xx标志、证据ID及序列覆盖,不伪造时间。
**关闭标准:**CRA-019/020/023/024;有缺口的487保留U,补回180后只转C;已接通但时长证据缺失时,A相关比率按不完整口径展示且不触发业务告警。
R03|P1|最终校正与乱序合并不满足到达顺序无关
**对应原文:**第3.4、4、6节。证据:已复现。
caller-analytics-model.ts:86–89 同时以“最终/暂定”和event.at决定更新。MI先取得在线快照、结束事件晚到时,可能已产生晚于真实结束时刻的暂定观察。最终零时长事件若event.at较早,会被忽略。
同一组事实的本轮输出:
| 输入到达顺序(t为统一起点) | 当前结果 | 应整改为 |
|---|---|---|
| ACCEPTED(t+1s) → DURATION(t+4s,3000ms) → RECONCILE(t+3s,0ms) | A=1,talkMs=3000 | A=0,talkMs=0 |
| ACCEPTED(t+1s) → RECONCILE(t+3s,0ms) → DURATION(t+4s,3000ms) | A=0,talkMs=0 | 与上一行相同 |
| ATTEMPT → UNKNOWN → END(487) | F=1,U=0 | 缺口尚未修复时U=1 |
| ATTEMPT → END(487) → UNKNOWN | F=0,U=1 | 与上一行相同 |
**整改:**将业务事件时间、接收时间、证据权威等级、校正版本分开。已核验最终时长优先于暂定时长;最终之间按来源修订版本选择,不能用消息到达顺序决定。未知缺口只能被对应补偿证据关闭。保留纯函数状态归约,但使同一事实集合的排列、重复、重领结果一致;更高版本纠错通过现有同事务贡献差值回写。
**关闭标准:**新增排列测试、显式零时长后迟到END/ACCEPTED测试;真实MySQL重放后events/state/buckets一致,A回退不产生负数,C按其独立有效证据保留。
**聚合层补充:**初始基线 caller-analytics-store.ts:46 将JSON路径 $.totalCalls 等作为JSON_OBJECT的键传入,而读取使用的是普通键totalCalls等。并行候选改动已把写入键修正为普通键,并新增从states重建分钟桶的维护脚本。这属于“修复代码已出现、历史聚合修复与真实验证待确认”,不能只发布新写入逻辑而留下旧桶。须核对受影响桶、states与events是否可信,备份后按第5节重建/差异核对;本轮没有执行该维护脚本。
R04|P1|运行心跳被当作数据完整性,lagMs=0缺少测量依据
**对应原文:**第4、5、6、10节。证据:源码确认。
- 初始基线的gap仅在内存;并行提交
d786165已将gap随游标保存并在启动恢复,原“重启必丢标志”问题已有修复代码。仍只有全局布尔值,没有受影响节点/时间范围与修复状态;保存失败、无完整新日志行、旧游标缺字段等边界仍需测试。 analytics-main.ts:57将最后处理的event.at作为dataThrough,乱序可倒退,且不代表此前无缺口。analytics-main.ts:89–95只看消费组pending/lag,正常时直接返回lagMs: 0;未计入未读日志、rsyslog队列或生产端停采。caller-analytics.service.ts:88–90以Worker心跳新鲜且未degraded标LIVE。即使OpenSIPS日志钩子失效,只要日志可读、MI可用且队列空,仍可能显示LIVE。
**整改:**增加生产端序列/心跳、日志未读字节、Redis最旧未处理年龄、消费速率、DB提交水位及持久化gap表。按节点维护“连续处理到哪个生产序列”,再合成查询窗口覆盖状态。没有可靠延迟证据时lagMs返回null;空闲与停采必须可区分。恢复进程只改变运行状态,不能自动修复历史缺口。
**关闭标准:**停采但不停Worker、积压日志但Stream暂空、轮转后重启、迟到旧事件四类场景均不误标完整;质量提示可按时间窗口查询,缺口修复有审计记录。
R05|P1|留存、持久化与资源隔离没有形成闭环
**对应原文:**第6、9节。证据:源码确认;线上参数待核验。
analytics-main.ts:45–49在XADD返回后推进本机游标,但这不等于Redis数据已经落盘。本地Redis基线infra/server-b/s04/redis/redis-lisglosips.conf:8–18为AOF everysec、1536MB、noeviction,实际生效值本轮未核实。everysec存在故障丢失最近约一秒写入的边界,见 Redis持久化文档。- 分析代码中未见Stream裁剪、专属日志轮转方案、事件/状态/桶/死信清理任务;rsyslog的256MB队列上限不是日志文件、Stream或MySQL表的保留期。
infra/server-b/caller-analytics/deploy.sh:64从cdr-worker环境复制REDIS_URL。独立Stream/消费组不能隔离同一Redis实例内存耗尽的影响。analytics-main.ts:52–55,84消费时直接JSON.parse并断言类型;缺少消费端schema校验和坏事件重试上限,也未处理XAUTOCLAIM可能返回的已删除pending ID列表。
Redis XACK移除的是消费组待确认记录,不能代替Stream数据删除。参见 Redis XACK 和 Streams恢复说明。
**整改:**建立已持久化消费水位与日志可重放区间,Redis恢复后识别回退并从保留日志补送。轮转按inode读取完旧文件后切换,游标含文件身份、序列、校验与持久化状态。为日志、Stream、events、states、buckets、deadletter分别定义容量、保留期、清理安全线和溢出行为;清理不得早于允许迟到/重放/去重窗口。超界必须登记缺口,不能静默丢弃。消费端校验输入,坏事件隔离并告警,正常事件继续推进。
资源方面先测共享Redis和MySQL余量;若无法证明统计流在故障与峰值下不影响呼叫/计费,则使用独立Redis实例或等效强隔离。仅换Redis DB编号不构成内存隔离。日志、死信含号码与Call-ID,须同步限定文件权限、访问范围、导出审计和脱敏保留规则。
**关闭标准:**断电/进程崩溃、XADD后游标提交、日志轮转、毒事件、满盘/满队列、重领/清理竞态均有真实恢复记录;数据可恢复或缺口可见,原呼叫与计费服务不受统计积压拖垮。
R06|P1|呼叫/尝试生命周期及真实出局边界不完整
**对应原文:**第3.1、4、6、12节。证据:源码确认,重试间隙已纯计算复现;真实重试尚未实施。
- 当前ATTEMPT在
t_relay()前发出(instrument-caller-analytics.mjs:21、路由基线opensips.cfg:307–315)。若本地转发失败且未实际发包,仍可能计入“实际发出的线路尝试”。 projectAnalytics():109–115把“当前所有leg结束”视为原始呼叫结束,没有业务呼叫关闭/不再重试事实。第一条失败、第二条尚未生成的间隙,原始视角提前判F。- 路由暂为单attempt=1,这是第12节已经承认的范围限制;现有多attempt单测证明能聚合两条记录,不能证明整个重试生命周期已支持。
observeActive()对MI中消失的呼叫没有UNKNOWN或对账处理;只有START或丢失END的记录可长期停在active/P。
**整改:**补充 CALL_CLOSED/retryExhausted、ATTEMPT_DISPATCHED、本地发送失败及终态对账契约。线路分母用可核验的实际派发事实;准备路由和尝试派发分开。原始呼叫只有被权威关闭且全部尝试判定完成时才判F。MI消失只表示需核对,不直接判业务超时;找不到终态依据转数据未知,并保留差异清单。
**关闭标准:**CRA-015/017/020;本地relay失败原始T=1、落地T按是否真实发送判断;重试间隙不提前判F;丢失END后不永久伪装处理中。真实重试启用前必须完成对应采集验收,不扩大当前路由功能范围。
R07|P2|身份、版本、号码标准化与对账字段缺少可执行契约
**对应原文:**第3.1、6、7节。证据:源码确认。
事件接口没有显式nodeId、bootId、事务方法/CSeq/branch、receivedAt、schemaVersion;日志解码时固定version=1、sequence=0。callUid由Call-ID、From-tag及OpenSIPS启动时间组合后哈希,并不是只用Call-ID,但仍缺少显式节点与客户隔离及事务重传/鉴权续发规则。不能因使用哈希就认定全球唯一。
状态和桶没有metricVersion/analysisRevision列,桶hash也不包含版本。号码直接使用caller/landingCaller,没有原值、规范值和规则版本的明确分离。RECONCILE虽在类型中存在,当前采集/Worker未见实际最终话单对账生产者;详情让用户按Call-ID查找,尚无稳定话单关联键。
**整改:**定义事件JSON Schema与日志映射,增加生产身份/事务/阶段/时间字段及规范化规则。保存原号码、用于分组的规范号码和规则版本;空维度采用文档化非空哨兵。增加事实修订与聚合版本,定义callUid/attemptId到原始话单的显式关联;来源无法核实时保留“未关联”,禁止Call-ID单字段猜配。旧固定6秒CDR继续禁止用于时长校准。
**关闭标准:**同号不同客户、跨节点相同Call-ID、重传、鉴权续发、主叫改写、升级前后重放均不串数;版本升级能并行核对和回退。
R08|P1|历史无覆盖与空窗口未区分,详情也未保持同一快照
**对应原文:**第3.5、4、5、7节及第12节补充。证据:源码确认。
overview()仅返回通用historyNotice;不按captureSince和缺口检查查询窗口。新采集前的窗口可返回T=0和LIVE,不能据此区分“没有呼叫”和“没有采集覆盖”。- overview使用一次数据库事务读取列表和趋势,这是已有正确基础;但asOf在查询后生成,详情另开查询,其rows与breakdown也不在同一快照。使用相同from/to只能保证发起窗口相同,不能冻结后续状态变更。
detail()的分布直接LIMIT 500,无ORDER BY、total或truncated字段,超过500组合会静默截断。- 列表后的客户、号码排序没有完整稳定的唯一兜底键,同号跨客户且指标相同时,分页稳定性需要补足。
**整改:**增加按查询窗口计算的coverage及各指标可用性;无覆盖返回null/NO_COVERAGE,不把未知历史展示成0。建议同一响应内生成总览与选中号码详情,或使用服务端短期snapshotId缓存聚合结果;明确“发起时间窗口”和“状态截至快照”的区别。详情查询也使用一致事务。分布改为稳定分页,或返回total/truncated并禁止将截断数据解释为完整总量。
第12节关于“卡片/趋势使用业务筛选,样本/比例/仅告警只筛号码列表”的调整合理,页面已有提示,可保留;但须把baseFilters与numberFilters写进正式API契约,避免与第3.5节混淆。
**关闭标准:**CRA-017/018/019/024;采集生效前、跨覆盖边界、非整分钟窗口、跨日、并发状态更新及>500组合均能正确解释并对账。
R09|P1|告警规则混合持续计数,且告警范围与查询范围不一致
**对应原文:**第8、12节。证据:源码确认。
analytics-alerts.ts:26–31每个“客户+号码+视角”只有一个streak;“低接通率→低总体应答率→低接通率”会累计成三次,即使没有同一规则连续满足。- 告警仅按号码全线路、最近15分钟计算;API按客户+号码附加ACTIVE告警,没有检查所选历史窗口、落地网关、地区等范围。筛选某条正常线路仍可能显示另一条线路造成的号码级告警。
- Worker在degraded时连评估与过期处理一起跳过,旧ACTIVE可持续保留;API未按更新时间或查询窗口过滤。健康恢复前不能将旧快照继续解释为当前业务异常。
- 当前实现有连续三次触发/恢复,但没有独立规则状态、明确冷却期、恢复阈值迟滞;同一窗口可每15秒重复评估,是否算独立持续证据未定义。
**整改:**按ruleId、客户、号码、视角、规则作用域和规则版本维护状态,分别记录触发计数、恢复计数、冷却、触发/恢复阈值与评估快照。采集异常时转SUSPENDED/STALE,保留历史原因但不继续当有效业务告警。若第一期只支持全线路15分钟号码级告警,应明确标注该作用域;历史或线路筛选下不冒充当前筛选结果的异常。
**关闭标准:**交替命中规则不误累计;同规则连续达到条件才触发;健康异常时暂停业务判断;查询历史/单线路时告警作用域清楚;仍不发送外部通知或自动处置号码。
R10|P2|页面与API有确定的契约差异,详情功能不完整
**对应原文:**第5、6节。证据:源码确认;本轮未做浏览器渲染验收。
- 页面用countColumns生成排序项,其中包含
notConnectedCalls(CallerAnalyticsPage.jsx:13,65),API白名单没有该字段(caller-analytics.service.ts:26),选择该排序会被400拒绝。 - 页面未展示API已有的metricVersion、lagMs、dataThrough、captureSince;仅展示通用质量状态和asOf。
- 供应商筛选在API有vendorId,页面没有;详情缺少选中号码的独立分钟趋势、180/183/直接成功统计分布、未接通原因聚合、接通未应答最终原因聚合和稳定CDR详情链接。
- 详情用单一
finalCode显示“最终结果”,采集挂断钩子填200、超时填408,可能把对话结束原因与初始INVITE最终响应混为一谈。 - 页面详情遇到“仅可信正时长RECONCILE补C、无180/183/2xx时间”时仍显示“未达接通”,与C/A计数不符。
- 自动刷新为请求完成后再等4000ms,与原文“每5秒刷新”不同;失败保留旧数据、筛选变化取消旧请求的逻辑可保留。
**整改:**共享字段/排序契约,支持N排序或去掉无效选项;补齐版本、覆盖时间、延迟未知说明。详情分离 initialInviteFinalCode、terminationReason、localFailureCode,显示真实connectedEvidence。补齐承诺功能;若分期延后,必须列出未交付项,不能写“页面已完成”概括替代。今日边界建议由服务端计算,统一Asia/Shanghai展示;自定义输入标明时区。保留宽表、业务数字直显、筛选和详情刷新语义。
**关闭标准:**逐一操作所有排序、筛选、分页、详情与失败重试;采集缺口和CDR补计显示一致;网络面板、真实API/DB与截图留证,静态页面不算验收。
R11|P2|实时SLO、查询成本和容量限制需要实测拆解
**对应原文:**第6、9、10、12节。证据:源码确认,容量待实测。
analytics-main.ts 将日志采集、串行消费、全量MI查询、活跃呼叫观察、告警循环放在同一循环;observeActive() 固定LIMIT 2000且无遍历游标,超过范围可能长期漏观察尾部呼叫。时长大于0后不再更新,进行中的talkMs只是首次正时长观察值,不是持续实时增长时长。
API overview每次读取最多10001号码后在应用内过滤排序,summary/numbers/trends均复用完整overview;10000上限限制结果数量,不等于限制数据库扫描成本。7天分钟趋势可达到10080点,需验证前端渲染成本。告警每号码还执行多次查询。状态更新会对已有多个投影反复撤销/添加桶贡献,吞吐不能仅按事件数量估计。
**整改:**先记录峰值CPS、事件/呼叫数、号码与维度基数、平均通话时长、迟到分布、保留期和资源余量,再确定索引与任务频率。将采集、消费、MI观察和告警按职责解耦或设独立预算;活跃观察游标遍历并暴露遗漏量。API用实际SQL执行计划核验,再决定数据库分页/汇总、短缓存和趋势降采样,缓存键必须含授权范围、筛选及快照/版本。
容量估算统一使用:事件写入率≈CPS×每呼叫事件数;留存空间使用实测平均事件字节、索引及复制/备份开销。消费恢复能力必须大于持续新增速率,不能只测空队列瞬时吞吐。
**关闭标准:**保留原目标“约定负载下证据可用至页面可见P95≤5秒”,分解采集、消费、查询、轮询等待与渲染延迟;本轮不给机器容量承诺。若轮询无法满足整体目标,调整轮询或推送,不能把SLO起点偷换为API返回时刻。记录原呼叫建立、计费延迟与资源回归。
R12|P1|发布脚本的完整回滚与再次发布行为需要补齐
**对应原文:**第9、12节。证据:源码确认,未执行部署。
infra/server-b/caller-analytics/deploy.sh 已有备份、候选配置校验、空闲通话检查、健康检查和加法迁移保留,这些应保留。但仍存在:
- 已含CRA1标记时直接沿用旧采集配置(41–46行附近),后续采集器修复不会自动进入生效配置;须有明确instrumentationVersion和差异审核流程。
- 初始rollback未恢复rsyslog配置/env及旧分析进程;并行提交
d786165已补恢复配置、重启rsyslog及按原active状态拉起分析服务。仍需恢复原enable/disable状态、验证每个失败点,不能仅凭代码补齐就认定已演练通过。 - 部分配置修改发生在changed=1之前,之后验证失败可能不进入完整回滚;只能证明主指针回退,不能证明所有步骤原子恢复。
- 开始时一次“当前无呼叫”检查与实际重启之间存在新呼叫进入窗口;这不是持续排空保证。
- 脚本末尾只检查服务active/API ready/commit,未验证分析权限、心跳、采集覆盖、端到端数据和持久化。不能把这些检查当完整功能验收。
**整改:**使用可重复执行的版本化采集配置生成;逐步登记变更与回滚动作,包括原配置存在性、服务enable/active状态、rsyslog重载、入口开关和DB版本。发布前后分别检查,并采用维护窗口/受控排空防止中途新呼叫。回滚默认保留分析事实与新表;恢复应用/配置/消费者组合,不回滚或删除计费账务。增加分析端到端冒烟门槛并验证服务自动启动关系。
**关闭标准:**在测试环境分别注入迁移后、rsyslog校验后、OpenSIPS安装后、API切换后、分析服务启动后的失败;每个步骤均能恢复原服务组合,保留证据且不影响既有账务。
R13|P1|“已实施”与“已验收”记录没有闭合
**对应原文:**文首、第2、10、11、12节;主设计与实施状态索引。证据:源码/本地报告目录确认。
原文用实施更新和第12节解释前文历史状态,但文首仍写全部待实施,第11节仍写仅文档交付;主设计V2第1.1节仍写实现待开始。IMPLEMENTATION_STATUS.md 顶部写测试发布验证中,测试计划仍注明CRA-001至024全部待执行。本轮在 tests/reports 未检索到专项真实验收报告;这只能说明本地未归档,不能断言远端从未执行。
初始 caller-analytics-real.mjs 主要断言六个本机SIP场景的聚合值、详情、401和非法参数400,“real DB states”主要来自API详情。收尾时并行候选代码已增加独立states SQL求和与API汇总核对、事件重复消费保持不变的断言;本轮只检查代码,未执行这些新增步骤,也未看到其真实报告。普通view客户越权、采集丢失、计费不重复、P95、真实最终CDR校正、故障恢复与发布回滚仍无完整证据;SIP脚本未带SDP。因此不可把“六场景通过”写成24项全通过或媒体/计费全链路验收。
**整改:**原文修订为设计文档V1.2,保留V1.1业务口径,增加明确“设计/本地实现/测试部署/真实验收/正式启用”状态表;原第11节移到历史记录。建立需求—实现—CRA用例—报告路径—提交/环境对应台账;每项标记通过、失败、未执行、范围外,不以测试文件存在代替执行。若真实证据在远端,核验后脱敏归档。
**关闭标准:**24项原CRA逐条有对应结果;新增边界有证据;发布报告包含commit、配置hash、迁移版本、环境、执行时间、回滚点及未通过项。没有证据的项继续标未验收。
4. 推荐目标契约与实施顺序
4.1 最小充分整改范围
保留现有“OpenSIPS观察 → 本机留存 → 独立Stream → 分析Worker → MySQL事实/状态/分钟桶 → API/页面”架构。先修事实与状态,不以重写整个平台、引入新消息中间件或修改计费作为前提。
最小需要触及:采集脚本与版本、分析Worker/模型/存储、加法数据库迁移、API/页面契约、专项测试、部署/回滚脚本与文档。是否增加独立Redis实例由R05容量及故障隔离验证决定,作为需提前报告的范围扩展。
固定6秒旧CDR属于独立计费/话单问题。本轮只规定禁止其作为统计权威时长;不自动重算历史费用、补扣费或变更账务。后续若整改该问题,须另列范围、影响评估与验收。
4.2 建议新增/明确的数据字段
下表为整改目标,不表示当前已存在。物理表拆分由实施时评审,优先加法兼容。
| 对象 | 最小补充内容 | 作用 |
|---|---|---|
| 事件事实 | schemaVersion、nodeId、bootId、producerSequence、occurredAt、receivedAt、初始事务身份、dispatch结果、source/evidenceId | 稳定关联、去重、追踪顺序与缺口 |
| 业务呼叫 | callUid、客户快照、businessStartedAt、closedAt、retryExhausted、callCloseEvidence | 原始视角的分母与结束条件 |
| 线路尝试 | attemptId、attemptStartedAt、dispatchedAt、first180At/183At、initialInviteFinalCode、terminationReason | 区分准备、发送、进展、最终响应与对话结束 |
| 时长证据 | talkStartedAt、talkEndedAt、talkDurationMs、durationStatus、sourceRevision、correctedAt | 暂定/最终/无效/未知及乱序校正 |
| 完整性 | 节点、起止序列/时间、gapId、影响范围、恢复/未恢复状态、处理水位 | 不以Worker活跃冒充数据完整 |
| 状态/分钟桶 | metricVersion、analysisRevision、numberNormalizationVersion、coverage信息 | 版本隔离、可回算与可回退 |
| 关联/告警 | 稳定CDR关联键;ruleId、scopeHash、ruleVersion、snapshotId、冷却/恢复状态 | 可追溯且不会混用范围 |
保留 metricVersion=caller-analytics-v1.1 表示业务公式未改变;增加analysisRevision表示算法/证据修复批次。若未来真正改变指标定义,才升级metricVersion。文档版本与业务口径版本不能混为一谈。
API建议在现有字段之外明确:snapshotId、coverageStatus、captureFrom、gapIntervals、metricAvailability、provisionalAnsweredCalls、durationUnknownCalls、baseFilters、numberFilters及分页完整性。asOf标数据库状态快照时间,dataThrough标连续覆盖水位,lagMs为测量值或null。
4.3 工作包与依赖
| 工作包 | 优先级/涉及项 | 产物 | 前置依赖与完成门槛 |
|---|---|---|---|
| W0 基线冻结 | P1,R13 | V1.2文档、需求/用例矩阵、实际部署只读核对表 | 确定生效配置、版本、授权测试范围,不实施业务变更 |
| W1 证据与状态 | P1,R01/R02/R03/R06;带R07字段 | 采集契约、确定性归约、真实会话时长验证 | 无法核验时长来源时,A不能宣称完整;单元边界先通过 |
| W2 完整性与可靠性 | P1,R04/R05 | gap、水位、补送/死信/留存、资源隔离 | 必须完成真实Redis/MySQL恢复与计费隔离验证 |
| W3 API/页面/告警 | P1/P2,R08/R09/R10/R11 | 快照、覆盖、告警状态机、契约对齐、性能报告 | 依赖W1/W2可信事实;页面不能用模拟数据验收 |
| W4 迁移/发布/验收 | P1,R12/R13 | 灰度发布、故障回滚记录、CRA证据和关闭台账 | P1全部关闭,P2未完成项须明确限制,不笼统全量验收 |
W0/W1是首批最小开工范围;W2前不得扩大长期采集留存或据此承诺“计费隔离”。数据库索引、批量策略、轮询/推送选择在实测后确定。本轮未获得当前规模、可用测试时段及现场依赖,不给出缺乏依据的人日或上线日期承诺。
5. 数据修复、迁移与回滚方案
- **只读盘点与备份:**核对当前release、OpenSIPS/Redis/MySQL版本及生效参数、分析表行数/体积、Stream长度/PEL、日志及游标、采集生效时间、账务基线;备份配置与分析证据。已有发布状态先记录再变更。
- **兼容加法迁移:**增加证据/覆盖/版本列或新表,不覆盖原始events,不删除既有raw/rated CDR;保留旧读路径。迁移前核对现有数据、唯一键及索引成本。
- **影子重放:**用新的analysisRevision写入独立状态/桶命名空间。按客户、号码、视角、发起分钟对比旧新T/C/A/F/P/U,输出每项差异对应的事件证据与修复原因。
- **不能恢复的历史数据:**旧事件缺少事务/时长/覆盖证据时,不因新代码部署而变成可信数据;登记未知/无覆盖,明确新证据生效边界。旧固定6秒不参与补算A。
- **灰度读取:**先内部测试客户及受控SIP入口,再扩大授权客户范围;先观察数据质量,业务告警默认抑制,满足完整性门槛后启用。测试呼叫限定已授权环境和目标,不发外部通知。
- **验收切换:**完成原CRA及新增用例、数据重放一致性、既有呼叫/计费回归、延迟和资源验证,再切换读取版本,归档数据覆盖起点和发布记录。
- **回滚:**关闭业务告警和新读入口,切回上一个匹配的应用/配置/数据读版本,恢复原服务组合。存在采集性能风险时再关闭采集钩子,登记停采缺口;保留事实和新表以便修复,不全库恢复覆盖期间产生的新话单与账务。
回算、清理和去重必须共用保留期契约:允许重放的时间范围不能超过去重身份的保留范围;若超出,只允许写入新的重建版本,不能重灌在线聚合造成重复累计。
6. 验收矩阵与证据要求
原CRA-001至CRA-024继续有效,本次不修改其业务预期。已有单测通过仅说明部分纯计算行为正确。
| 验收组 | 关联原用例 / 新增建议编号 | 必须提供的证据 |
|---|---|---|
| 接通/应答基础语义 | CRA-001~009、011~014、018、021~023 | 真实SIP报文与事务匹配、会话时长来源、API快照、状态及桶SQL |
| 去重与多视角 | CRA-010、015~017 | 同一业务呼叫多尝试、重传/鉴权续发、同号不同客户与重领;DB及账务不重复 |
| 历史/故障/未知 | CRA-019/020/024 | 缺过程终态、缺整个呼叫、失联/轮转/重启、补送溢出及覆盖提示 |
| 时长来源边界 | CRA-025(建议新增) | 200无ACK、协商失败、早期媒体与真实500ms/0ms,区分可信/暂定/未知 |
| 最终校正乱序 | CRA-026(建议新增) | 本文R03两种顺序、重复、迟到END/ACCEPTED,终态和聚合完全一致 |
| 采集完整性 | CRA-027(建议新增) | Worker存活而生产端停采、日志积压、gap跨重启、水位乱序不误报LIVE |
| 留存与毒事件 | CRA-028(建议新增) | XADD后故障、游标恢复、轮转旧文件、坏schema、PEL被裁剪;不静默丢失 |
| 原始呼叫关闭 | CRA-029(建议新增) | 未真实出局、重试间隙、丢END后MI消失,不误算落地T和原始F |
| 快照与覆盖 | CRA-030(建议新增) | 生效前窗口、跨覆盖、非整分钟、跨日、>500组合及同步详情 |
| 告警状态机 | CRA-031(建议新增) | 交替规则、冷却、迟滞、健康暂停、历史/线路筛选作用域 |
| 权限与页面 | CRA-032(建议新增) | view但无scope、单客户scope、view_all、401/403;overview/detail/options均校验;全部排序及详情证据显示 |
| 容量与隔离 | CRA-033(建议新增) | 按约定CPS/基数/保留期记录端到端P95和原有呼叫/计费指标;活跃观察超2000不漏 |
| 发布与回滚 | CRA-034(建议新增) | 发布每个失败点、二次采集脚本升级、服务自动启动、配置与应用版本恢复 |
真实验收每个用例至少记录:runId、时间/环境、提交与配置hash、脱敏callUid/attemptId/Call-ID、预期计数、SIP与会话证据、API请求/返回、DB事件/状态/分钟桶查询、CDR关联或不能关联的原因、结果与报告路径。涉及计费隔离的用例,还必须独立核对原始话单、计费话单、余额变更和消费去重,而非只看分析API。
建议统一归档到 tests/reports/caller-analytics/<runId>/;真实数据按最小必要原则脱敏。没有覆盖的用例必须显式未执行,不用“正常”或“已支持”代替结果。
7. 原设计文档的逐节修订清单
此处给出待实施修订,不在本次直接重写原文,避免把建议误记为已落实。
| 原位置 | 建议修订 |
|---|---|
| 文首、第11/12节 | 当前状态集中成表;第11节移为历史交付记录,第12节事实附环境/时间/证据路径;设计版本V1.2、业务口径仍V1.1 |
| 第2节 | 改为“已有实现与未关闭差距”,准确列出分析表/Worker/API/页面,不再统称待新增 |
| 第3.1节 | 明确派发边界、业务呼叫关闭、单attempt现状与未来重试前置门槛 |
| 第3.3/3.5节 | 区分U、覆盖缺口、A证据不完整,明确null/暂定规则与baseFilters/numberFilters |
| 第3.4节 | 写明经过验证的时长来源、权威等级和校正版本,去掉以标签或差值自动认定“实际”的歧义 |
| 第4节 | 增加确定性归约、业务关闭、水位和时钟域;明确快照不等于窗口结束时间 |
| 第5节 | 增加页面已交付/待补列表;补供应商、详情、证据、延迟与覆盖展示 |
| 第6节 | 把建议事件名与实际START/ATTEMPT等做映射;补字段Schema、版本唯一键、补送/死信/保留期与对账生产者 |
| 第7节 | 规定新覆盖生效时间、不可修复历史规则、算法修复批次及影子回算方法 |
| 第8节 | 逐规则持续计数、范围、评估周期、冷却/迟滞、健康暂停及恢复条件 |
| 第9节 | 引用逐步发布/回滚清单和资源隔离门槛,不以服务active等同功能正常 |
| 第10节 | 保留24项原用例,增补CRA-025~034并链接真实报告;P95仍为待测目标 |
| 相关索引 | 同步主设计V2第1.1节、IMPLEMENTATION_STATUS和TEST_PLAN_AND_CASES,历史状态不覆盖当前状态 |
8. 本轮验证记录与复现方法
8.1 已执行的专项测试
执行本地现有依赖,不安装/升级依赖:
node node_modules/vitest/vitest.mjs run packages/database/src/caller-analytics.spec.ts apps/api/src/modules/caller-analytics/caller-analytics.service.spec.ts
本轮2026-08-31 17:32(Asia/Shanghai)结果:2个文件通过,16项测试通过;其中模型13项、查询参数3项。未在本轮重跑全量114项、构建或现场测试,因此不能引用旧全量结果作本轮验收。
8.2 纯计算复现
本轮用本地TypeScript转译源码后以内存模块加载 foldAnalytics/projectAnalytics,不实例化PrismaClient、不连接Redis、不读环境密钥、不调用SIP。以下等价复现可在项目支持TypeScript导入的Node/tsx环境执行;事件均为构造输入,用于证明算法边界,不能代替真实业务验收。
import { foldAnalytics, projectAnalytics } from './packages/database/src/caller-analytics-model.ts';
const t = Date.UTC(2026, 7, 31);
const event = (kind, extra = {}) => ({
eventId: kind, callUid: 'review-u1', callId: 'review-c1', attemptId: '1',
kind, at: t + 1000, startedAt: t,
customerId: 'review-c', customerGatewayId: 'cg', vendorId: 'v', vendorGatewayId: 'vg',
caller: '123', landingCaller: '456', callee: '789', city: 'x', carrier: 'UNKNOWN',
code: 0, talkMs: null, source: 'opensips', sequence: 0, ...extra
});
const run = events => projectAnalytics(events.reduce((c, e) => foldAnalytics(c, e), null))[0];
const attempt = event('ATTEMPT');
const accepted = event('ACCEPTED', { code: 200 });
const provisional = event('DURATION', { at: t + 4000, talkMs: 3000 });
const finalZero = event('RECONCILE', { at: t + 3000, talkMs: 0, source: 'verified-dialog-final' });
const end = event('END', { code: 487, source: 'verified-dialog-final' });
console.log(run([end])); // 当前F=1,U=0
console.log(run([attempt, event('UNKNOWN'), end])); // 当前F=1,U=0
console.log(run([attempt, end, event('UNKNOWN')])); // 当前F=0,U=1
console.log(run([attempt, accepted, provisional, finalZero])); // 当前A=1,talkMs=3000
console.log(run([attempt, accepted, finalZero, provisional])); // 当前A=0,talkMs=0
另两组本轮纯计算观察:第一尝试失败且尚无业务关闭事实时原始F=1;只有ACCEPTED及32秒后的对话END、无独立会话确认事实时A=1、talkMs=32000。前者证明多attempt生命周期仍不足;后者证明当前模型依赖信令时间差,不能单独证明实际通话成立。
8.3 本轮没有完成的验证
未读取线上活动配置、未执行真实MySQL/Redis查询与事务重放、未运行SIP脚本、未验收UI渲染、未做容量/故障/回滚演练。本文件是审查与整改方案,所有整改工作包尚待实施,不应据此标记缺陷已修复或功能已上线。
9. 主要依据索引
行号对应本次源码基线,后续修改后以函数/路由名及提交为准。
| 本地文件 | 核查用途 |
|---|---|
| 原设计 | 业务语义、实现补充、24项CRA与交付边界 |
| 状态模型 | 60–129行,事实合并、时长校正、投影、比率 |
| 存储事务 | consume/bucket/state,事务去重和贡献差值 |
| 分析Worker | 18–98行,日志/Stream/MI/健康与恢复 |
| 告警 | 3–35行,窗口、规则状态及过期 |
| 采集脚本 | 11–38行,身份、挂钩、阶段和结束事件 |
| API | parseAnalyticsQuery/overview/detail/options,筛选、权限、快照、分布及覆盖 |
| 页面 | 排序、刷新、展示与详情契约 |
| 迁移 | 表、索引、版本字段与权限 |
| 发布脚本 | 备份、迁移、配置切换、回滚与发布门槛 |
| Redis基线 | 本地持久化/内存配置参照,不代表现场 |
| 真实测试入口 / SIP脚本 | 六场景覆盖范围及缺失证据 |
| 实施状态 / 主设计V2 / 测试计划 | 状态一致性与验收索引 |
外部依据使用本轮查阅的RFC及OpenSIPS/Redis官方文档;不据此猜测现场软件版本或配置。所有方案中的生产变更、受控呼叫、数据回算和发布,都应在后续实施任务的实际授权范围内执行。