PAYATHON 2026

Laravel payment ID 在 live server 中被错误存为负数

支付阿杰

结论

这不是 Laravel 随机修改了 payment ID,而是典型的 32 位有符号整数溢出。

3104632297 超出了 32 位有符号整数的最大值:

2^31 - 1 = 2147483647

如果数据库字段、PHP 运行环境或 Eloquent 类型转换将这个值按 32 位有符号整数处理,最高位可能会被当作符号位,结果就会变成负数。

需要检查的地方

先查看线上数据库的字段定义:

SHOW COLUMNS FROM orders LIKE 'order_id';

以下类型无法安全保存这个 ID:

INT
INTEGER
INT SIGNED

再检查 Eloquent 模型中是否有整数转换:

protected $casts = [
    'order_id' => 'integer',
];

如果线上运行的是 32 位 PHP,这种转换也可能造成溢出。可以这样检查:

dump(PHP_INT_SIZE, PHP_INT_MAX);

64 位 PHP 通常会输出:

8
9223372036854775807

如果 PHP_INT_SIZE 为 4,说明 PHP 整数只有 32 位。

推荐解决方案

payment ID 是外部系统的标识符,不需要参与数学计算。即使现在只包含数字,也更适合按字符串保存。

可以通过 Laravel migration 将字段改为字符串:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('orders', function (Blueprint $table) {
            $table->string('order_id', 64)->change();
        });
    }
};

更新数据时,显式转换为字符串:

$order->update([
    'order_id' => (string) $response->json['id'],
]);

return $response;

模型中也不要再将它转换成 integer:

protected $casts = [
    'order_id' => 'string',
];

这样还能兼容将来可能出现的前导零、字母或更长的 ID。

如果必须使用整数类型

如果业务能明确保证 payment ID 始终是非负数字,可以在 MySQL 中使用 BIGINT UNSIGNED:

Schema::table('orders', function (Blueprint $table) {
    $table->unsignedBigInteger('order_id')->change();
});

对应的 MySQL 取值范围是:

0 到 18446744073709551615

但 32 位 PHP 仍然无法将大整数安全地作为原生 int 处理。因此,即使数据库使用 BIGINT UNSIGNED,应用层最好仍将 payment ID 作为字符串传递,避免使用 intval()、(int) 或 Eloquent 的 integer cast。

检查 JSON 解码过程

如果 gateway 返回的 ID 是 JSON 数字,32 位 PHP 在解码大整数时也可能丢失精度。如果条件允许,应让 provider 直接以字符串形式返回 ID:

{
  "id": "3104632297"
}

如果需要自行解码原始 JSON,可以把超大整数保留为字符串:

$data = json_decode(
    $responseBody,
    true,
    512,
    JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING
);

$order->update([
    'order_id' => (string) $data['id'],
]);

这样做的前提是代码能够取得未经二次处理的原始 JSON。如果 HTTP 客户端已经完成解码,需要检查它的 JSON 解码配置。

为什么本地正常、线上异常

这通常说明两套环境存在差异,例如:

  • 本地字段是 BIGINT 或 VARCHAR,线上字段仍是 INT
  • 本地已经执行 migration,线上尚未执行
  • 本地使用 64 位 PHP,线上使用 32 位 PHP
  • 线上模型或数据层还有额外的整数类型转换
  • 两边数据库的字段定义或 SQL mode 不一致

所以不能只比较 Laravel 代码,还需要直接核对数据库 schema、PHP 架构和模型 $casts。

最稳妥的做法是使用 VARCHAR 数据库字段,在 Laravel 中设置 string cast,并在整个处理过程中将 payment ID 视为不透明字符串。

备注:内容仅供参考。