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 视为不透明字符串。
备注:内容仅供参考。
