PHP项目短信账单如何统计核对条数

wen PHP项目 30

PHP项目短信账单统计核对条数实战指南:从数据埋点到自动化对账

目录导读

  1. 短信账单统计核对的痛点与价值
  2. 核心统计逻辑与数据库设计
  3. 基于API日志的条数精确统计方法
  4. 账单核对的双向校验算法
  5. 实战代码:PHP统计核对模块
  6. 常见问题与QA解答

痛点与价值

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

PHP项目短信账单如何统计核对条数

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 全量统计策略

核心原则:以本地发送日志为准,以通道回调为佐证,通过双向匹配确认条数。

统计步骤

  1. 从PHP应用中提取sms_log表中时间段内的所有记录
  2. channel_reply_id去重,避免重复计数(同一短信重发导致)
  3. 汇总本地计算的segments总和
  4. 从通道商API导出对账单(CSV/Excel格式)
  5. 将通道账单中的条数与本地汇总值做差运算

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去重、定期自动对账,建议每月初自动生成对账报告,并与通道商确认差异,避免因统计偏差导致长期成本损失。

抱歉,评论功能暂时关闭!