PHP项目短信账单统计核对条数实战指南:从数据埋点到自动化对账
目录导读
痛点与价值
短信服务已成为企业触达用户的核心渠道,但每月数十万乃至千万级的发送量,让账单核对成为技术团队的棘手难题,常见痛点包括:通道商返回成功但实际未送达、长短信拆分规则不一致、批量发送时统计重复,据统计,约30%的企业每月因未核对短信条数而多付5%-15%的费用。

PHP项目在处理短信账单时,必须解决三个核心问题:
- 条数统计的准确性:单条短信按67字(中文)或160字符(英文)拆分为多条
- 通道侧与业务侧数据一致性:对方账单中的条数与本地记录必须匹配
- 对账流程自动化:避免人工逐条比对的低效与出错
核心统计逻辑与数据库设计
1 数据表结构示例
CREATE TABLE `sms_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `content` text COMMENT '短信内容', `segments` int(4) DEFAULT '1' COMMENT '拆分后的真实条数', `channel_reply_id` varchar(100) DEFAULT NULL COMMENT '通道返回ID', `status` tinyint(1) DEFAULT '0' COMMENT '0:待确认 1:已送达 2:失败', `create_time` datetime DEFAULT NULL, `channel_cost` decimal(10,2) DEFAULT '0.00' COMMENT '通道计费条数', PRIMARY KEY (`id`), KEY `phone` (`phone`), KEY `create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关键字段解析:
segments:本地根据内容长度计算的条数channel_cost:通道接口返回的实际计费条数- 二者差异即为核对的关键点
2 条数计算函数
/**
* 计算短信拆分后的实际条数
* @param string $content 短信内容
* @param int $maxSingleLen 单条最大字符数(中文67,英文160)
* @return int 返回拆分后的总条数
*/
function getSmsSegments($content, $maxSingleLen = 67) {
// 检测是否包含中文(非ASCII字符)
$isChinese = preg_match('/[\x{4e00}-\x{9fa5}]/u', $content) > 0;
$maxLen = $isChinese ? 67 : 160;
// 部分通道商按67字计费,若超长则自动拆分
$length = mb_strlen($content, 'UTF-8');
if ($length <= $maxLen) return 1;
// 计算拆分数量(向上取整)
return ceil($length / $maxLen);
}
基于API日志的条数精确统计方法
1 全量统计策略
核心原则:以本地发送日志为准,以通道回调为佐证,通过双向匹配确认条数。
统计步骤:
- 从PHP应用中提取
sms_log表中时间段内的所有记录 - 按
channel_reply_id去重,避免重复计数(同一短信重发导致) - 汇总本地计算的
segments总和 - 从通道商API导出对账单(CSV/Excel格式)
- 将通道账单中的条数与本地汇总值做差运算
2 自动对账脚本逻辑
class SmsBillingChecker {
private $timeStart;
private $timeEnd;
public function checkByTime($start, $end) {
// 1. 获取本地统计
$localStats = $this->getLocalStats($start, $end);
// 2. 获取通道账单数据(需对接通道商API)
$channelStats = $this->getChannelBilling($start, $end);
// 3. 对比差异
$diff = abs($localStats - $channelStats);
if ($diff > 0) {
// 记录差异详情,便于人工复核
$this->logDiffRecords($start, $end, $diff);
}
return $diff;
}
private function getLocalStats($start, $end) {
$sql = "SELECT SUM(segments) as total FROM sms_log
WHERE create_time BETWEEN ? AND ? AND status = 1";
// PDO查询省略...
return $total;
}
}
账单核对的双向校验算法
1 常见差异原因与处理
| 差异类型 | 原因 | 解决方案 |
|---|---|---|
| 本地条数>通道条数 | 长短信拆分规则不一致 | 统一采用通道商的计费规则 |
| 本地条数<通道条数 | 通道重复计费或加收签名费用 | 核对签名是否单独计费 |
| 通道有记录、本地无 | 代发漏记录或数据库写入失败 | 增加通道回调写入机制 |
2 自动匹配算法
/**
* 双向比对逻辑
* @return array ['matched'=>int, 'only_local'=>array, 'only_channel'=>array]
*/
function bilateralMatch($localList, $channelList) {
$matched = 0;
$onlyLocal = [];
$onlyChannel = [];
// 以手机号+发送时间作为唯一标识(秒级精度)
foreach ($localList as $l) {
$key = $l['phone'] . '_' . date('YmdHi', strtotime($l['create_time']));
if (isset($channelList[$key])) {
// 检查条数是否一致
if ($l['segments'] == $channelList[$key]['segments']) {
$matched++;
} else {
$onlyLocal[] = $l; // 条数不符,标记
}
unset($channelList[$key]);
} else {
$onlyLocal[] = $l;
}
}
// 剩余的全是通道独有记录
$onlyChannel = array_values($channelList);
return compact('matched', 'onlyLocal', 'onlyChannel');
}
实战代码:PHP统计核对模块
1 定时任务方案(Crontab + PHP脚本)
// /path/to/cron/sms_billing_check.php
require_once __DIR__ . '/config.php';
$date = date('Y-m-d', strtotime('-1 day')); // 核对昨天的账单
$checker = new SmsBillingChecker();
$diff = $checker->checkByTime($date . ' 00:00:00', $date . ' 23:59:59');
if ($diff > 0) {
// 发送钉钉或邮件告警
$alertMsg = "短信账单差异告警:{$date} 差异{$diff}条,请人工核查";
// 可集成钉钉Webhook或邮件
DingTalk::send($alertMsg);
}
2 可视化报表接口(ThinkPHP示例)
public function billingReport() {
$start = input('start', date('Y-m-01'));
$end = input('end', date('Y-m-d'));
$localTotal = Db::name('sms_log')
->whereTime('create_time', 'between', [$start, $end . ' 23:59:59'])
->where('status', 1)
->sum('segments');
// 假设已导入通道对账单到本地表
$channelTotal = Db::name('channel_billing')
->whereTime('billing_date', 'between', [$start, $end])
->sum('billed_segments');
return json([
'code' => 0,
'data' => [
'local' => (int)$localTotal,
'channel' => (int)$channelTotal,
'diff' => (int)($channelTotal - $localTotal)
]
]);
}
常见问题与QA解答
Q1:长短信拆分规则不同怎么办?
不同通道商对短信拆分有差异,中国移动官方按67字拆分,但部分第三方按70字计费。建议:统计时取二者最大值,并在核对时以通道商回传的计费条数为准,本地记录仅作参考。
Q2:同一号码重复接收导致条数统计翻倍如何处理?
在统计前必须根据channel_reply_id或发送批次号去重,PHP代码中可使用array_unique或数据库GROUP BY channel_reply_id实现。
Q3:如何避免漏记回流数据?
建议采用双写策略:调用通道API发送成功后立即记录本地日志,同时监听通道回调地址更新状态,当回调未收到时,启用5分钟重试机制,并记录重试次数。
Q4:月账单差异超过1%怎么办?
立即启用逐条比对模式:导出通道侧CSV与本地日志,使用前述bilateralMatch函数进行精确匹配,重点关注:
- 85字以上的长短信
- 包含特殊符号(如emoji)的短信
- 批量发送时段(可能因并发写入冲突导致漏记)
Q5:推荐使用什么工具辅助核对?
PHP项目可直接集成Excel导出功能将差异数据导出,配合Chrome插件SheetMagic可快速筛选,若数据量超过10万条,建议使用Laravel队列分批处理,避免内存溢出。
通过上述方法,PHP开发者可构建一套从统计埋点>自动计算>双向校验>告警通知的完整短信账单核对系统,核心在于:统一规则、基于唯一ID去重、定期自动对账,建议每月初自动生成对账报告,并与通道商确认差异,避免因统计偏差导致长期成本损失。