PHP对账文件怎么处理

wen PHP项目 5

本文目录导读:

PHP对账文件怎么处理

  1. 文件读取与解析(核心步骤)
  2. 数据标准化与比对逻辑
  3. 生成对账报表
  4. 进阶处理技巧与注意事项
  5. 完整实战示例(命令行脚本或定时任务)
  6. 总结要点(精华部分)

在PHP中处理对账文件(通常是CSV、Excel或固定格式的文本文件),通常涉及以下步骤:读取文件 → 解析数据 → 比对数据 → 生成差异报告

以下是一个从基础到进阶的完整处理方案,包含代码示例和常见坑点规避。


文件读取与解析(核心步骤)

对账文件常见格式为 CSVTXT(以逗号/制表符/竖线分隔)。推荐使用 fgetcsv 而不是 explode 因为它能正确处理引号包裹的字段和转义字符。

处理 CSV / 制表符分隔文件

<?php
function parseFile(string $filePath, string $delimiter = ','): array
{
    $rows = [];
    if (!file_exists($filePath)) {
        throw new Exception("文件不存在: " . $filePath);
    }
    if (($handle = fopen($filePath, 'r')) !== false) {
        // 如果需要跳过表头,取消下面代码注释
        // fgetcsv($handle, 0, $delimiter); // 跳过第一行(表头)
        while (($data = fgetcsv($handle, 0, $delimiter)) !== false) {
            // 过滤空行(全为空或者只有一个空字符串)
            if (count($data) <= 1 && trim($data[0] ?? '') === '') {
                continue;
            }
            $rows[] = $data;
        }
        fclose($handle);
    }
    return $rows;
}
// 使用示例:解析竖线分隔符(|)的对账文件
$localRows = parseFile('/path/to/local_payments.csv', '|');

处理固定宽度文本文件(银行流水常见)

如果每行没有分隔符,只有固定长度的字符串,则需要按长度切割。

function parseFixedWidth(string $line, array $widths): array
{
    $fields = [];
    $offset = 0;
    foreach ($widths as $width) {
        $fields[] = trim(substr($line, $offset, $width));
        $offset += $width;
    }
    return $fields;
}
// 假设银行文件格式:日期(10) 流水号(15) 金额(12) 状态(5)
$line = "20231001   ABC1234567   100.50  SUCC";
$fields = parseFixedWidth($line, [10, 15, 12, 5]); 

处理 Excel 文件(.xlsx)

需要安装第三方库 phpoffice/phpspreadsheet

composer require phpoffice/phpspreadsheet
use PhpOffice\PhpSpreadsheet\IOFactory;
$spreadsheet = IOFactory::load($filePath);
$rows = $spreadsheet->getActiveSheet()->toArray();

数据标准化与比对逻辑

读取文件后,必须做数据清洗(去除空格、货币符号、数字转换),然后再进行比对。

比对策略(常见情况)

  • 以我方系统为主:遍历本地数据,查找对方文件中对应的订单号。
  • 以对方文件为主:遍历上传文件,在本地数据库中查找。
<?php
function compareFiles(array $localRows, array $remoteRows): array
{
    // 1. 构建一个索引:以唯一订单号作为Key(假设字段1是订单号)
    $localIndex = [];
    foreach ($localRows as $row) {
        $orderId = trim($row[0]);
        $localIndex[$orderId] = [
            'amount' => floatval($row[2]), // 假设金额在第三列
            'raw' => $row
        ];
    }
    $mismatches = []; // 差异记录
    // 2. 遍历对方文件进行比对
    foreach ($remoteRows as $remoteRow) {
        $remoteOrderId = trim($remoteRow[0]);
        $remoteAmount = floatval($remoteRow[2]);
        // 情况A:对方文件中的订单,本地不存在
        if (!isset($localIndex[$remoteOrderId])) {
            $mismatches[] = [
                'order_id' => $remoteOrderId,
                'type' => 'REMOTE_ONLY',  // 我方缺少此记录
                'expected_amount' => null,
                'actual_amount' => $remoteAmount,
                'message' => '交易在我方系统中不存在'
            ];
            continue;
        }
        // 情况B:金额不一致
        $localRecord = $localIndex[$remoteOrderId];
        if (abs($localRecord['amount'] - $remoteAmount) > 0.001) { // 浮点比较
            $mismatches[] = [
                'order_id' => $remoteOrderId,
                'type' => 'AMOUNT_MISMATCH',
                'expected_amount' => $localRecord['amount'],
                'actual_amount' => $remoteAmount,
                'message' => '金额不一致'
            ];
        }
        // 情况C:比对成功,从待检查列表中移除
        unset($localIndex[$remoteOrderId]);
    }
    // 3. 遍历剩余的本地记录:本地有,对方文件缺失
    foreach ($localIndex as $orderId => $localRecord) {
        $mismatches[] = [
            'order_id' => $orderId,
            'type' => 'LOCAL_ONLY', // 对方缺少此记录
            'expected_amount' => $localRecord['amount'],
            'actual_amount' => null,
            'message' => '交易在渠道文件中缺失'
        ];
    }
    return $mismatches;
}

生成对账报表

将差异结果导出为 CSV 或 Excel 便于财务人员查看。

function generateReport(array $mismatches, string $outputPath): void
{
    $handle = fopen($outputPath, 'w');
    fputcsv($handle, ['订单号', '差异类型', '我方金额', '对方金额', '说明']);
    foreach ($mismatches as $mismatch) {
        fputcsv($handle, [
            $mismatch['order_id'],
            $mismatch['type'],
            $mismatch['expected_amount'],
            $mismatch['actual_amount'],
            $mismatch['message'],
        ]);
    }
    fclose($handle);
}

进阶处理技巧与注意事项

1 处理大文件(防止内存溢出)

如果文件有几十万行,不要让 file()file_get_contents() 一次性读取全部内容。必须使用流式读取(逐行处理)

// 使用生成器或直接在循环中处理
while (($data = fgetcsv($handle, 0, ",")) !== false) {
    // 在这里直接比较或写入临时数组,但需注意内存
    // 更好的方式:直接写数据库临时表,然后用SQL比对
}

推荐方案:将两条文件的数据直接 导入 MySQL 临时表,使用 LEFT JOIN 进行比对,速度最快,且不耗 PHP 内存。

-- 创建临时表导入本地数据
CREATE TEMPORARY TABLE tmp_local (order_id VARCHAR(50), amount DECIMAL(10,2));
-- 使用 LOAD DATA INFILE 导入
LOAD DATA INFILE '/path/to/local.csv' INTO TABLE tmp_local 
FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';

2 金额比较的精度问题

千万不要用 或 比较浮点数,使用 abs($a - $b) < 0.01(PHP中有bccomp()函数更准确):

if (bccomp((string)$localAmount, (string)$remoteAmount, 2) !== 0) {
    // 金额不一致
}

3 编码问题

渠道传上来的文件可能是 GBK 编码,需要转成 UTF-8

// 读取时转换编码
$row = array_map(function($field) {
    return mb_convert_encoding($field, 'UTF-8', 'GBK');
}, $data);

4 对账的“状态机”思想

如果涉及到支付平台,通常会有一个 ReconciliationLog 表,记录:

  • 文件哈希值(防止重复处理)
  • 处理开始时间
  • 处理结束时间
  • 导入总条数
  • 匹配成功数
  • 差异数
  • 文件路径

第一步先去数据库查这个文件是否已经处理过,防止重复对账造成脏数据。

// 伪代码
if (ReconLog::where('file_hash', md5_file($filePath))->exists()) {
    throw new Exception('该对账文件已经处理过,请勿重复上传');
}

完整实战示例(命令行脚本或定时任务)

<?php
/* reconcile.php */
declare(strict_types=1);
require 'vendor/autoload.php'; // 如果用了依赖
class ReconcileService 
{
    public function process(string $localFile, string $channelFile): array
    {
        // 1. 解析本地数据(数据库或CSV)
        $localRows = $this->parseCSV($localFile, '|');
        $channelRows = $this->parseCSV($channelFile, ',');
        // 2. 核心比对逻辑(复用上面的 compareFiles)
        $diff = $this->compareFiles($localRows, $channelRows);
        // 3. 生成报告
        $this->generateReport($diff, '/tmp/recon_'.date('YmdHis').'.csv');
        // 4. 记录日志
        $this->logReconResult(count($localRows), count($channelRows), count($diff));
        return $diff;
    }
    private function parseCSV(string $path, string $delimiter): array
    {
        // ... 前面解析代码 ...
    }
    private function compareFiles(array $local, array $channel): array
    {
        // ... 前面比对代码 ...
    }
}
// 命令行执行
$service = new ReconcileService();
$differences = $service->process('local.csv', 'channel.csv');
echo "对账完成,发现 " . count($differences) . " 条差异\n";

总结要点(精华部分)

  1. fgetcsv() 处理带引号的字段,explode() 不行
  2. 大文件必须流式处理,避免 file_get_contents() 导致内存溢出。
  3. 浮点数比较要用 bccomp() 或误差范围比较,禁止用 。
  4. 构建索引(订单号 => 数据),避免双层 foreach 循环(时间复杂度 O(n²) 会导致慢)。
  5. 文件去重机制:记录文件 Hash,防止重复导入。

如果需要处理多种渠道(支付宝、微信、银行)不同格式,可以定义一个接口(如 FileParserInterface),为每个渠道写一个具体的解析器类,使用策略模式来管理,这样后续加渠道会非常方便。

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