PAYATHON 2026

如何在 Laravel 8 中实现预约定金或首付款?

支付小周

结论

Laravel 8 没有内置的“定金支付”功能。可以在预约业务中加入付款类型和定金比例,再用服务价格乘以所选比例,算出本次应付金额。

有一条规则不能省:服务器必须根据数据库中的服务价格重新计算金额,不能信任前端提交的金额。 如果最低支付比例是 30%,服务器就只能接受 30% 及以上的选项,不能允许用户提交 20%。

假设服务价格为 50€:

  • 支付 30%:50 × 30% = 15€
  • 支付 50%:50 × 50% = 25€
  • 全额支付:50 × 100% = 50€

数据库设计

金额最好用最小货币单位保存。欧元可以按“分”存储,避免浮点数带来的计算误差。

可以在预约表中增加这些字段:

// database/migrations/xxxx_xx_xx_add_payment_fields_to_appointments_table.php

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

class AddPaymentFieldsToAppointmentsTable extends Migration
{
    public function up()
    {
        Schema::table('appointments', function (Blueprint $table) {
            $table->unsignedBigInteger('service_price')->comment('预约时的服务价格,单位:分');
            $table->unsignedTinyInteger('payment_percentage')->default(30);
            $table->unsignedBigInteger('amount_due')->comment('本次应付金额,单位:分');
            $table->unsignedBigInteger('amount_paid')->default(0);
            $table->string('payment_status')->default('pending');
            $table->string('payment_reference')->nullable();
        });
    }

    public function down()
    {
        Schema::table('appointments', function (Blueprint $table) {
            $table->dropColumn([
                'service_price',
                'payment_percentage',
                'amount_due',
                'amount_paid',
                'payment_status',
                'payment_reference',
            ]);
        });
    }
}

然后执行:

php artisan migrate

预约中需要保存 service_price。这样即使服务价格后来发生变化,已创建的预约仍会保留当时的价格。

提供付款选项

假设最低定金比例是 30%,页面可以提供 30%、50% 和全额付款三个选项:

<form method="POST" action="{{ route('appointments.store') }}">
    @csrf

    <input type="hidden" name="service_id" value="{{ $service->id }}">

    <label for="payment_percentage">付款方式</label>

    <select name="payment_percentage" id="payment_percentage" required>
        <option value="30">支付 30% 定金</option>
        <option value="50">支付 50%</option>
        <option value="100">全额支付</option>
    </select>

    <button type="submit">创建预约并付款</button>
</form>

前端可以展示预计金额,但它只能作为提示。实际支付金额仍要由控制器重新计算。

在服务器端验证和计算金额

// app/Http/Controllers/AppointmentController.php

namespace App\Http\Controllers;

use App\Models\Appointment;
use App\Models\Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;

class AppointmentController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'service_id' => ['required', 'integer', 'exists:services,id'],
            'payment_percentage' => [
                'required',
                'integer',
                Rule::in([30, 50, 100]),
            ],
        ]);

        $service = Service::findOrFail($validated['service_id']);

        // 假设 price 保存的是欧分,例如 50€ 保存为 5000。
        $servicePrice = (int) $service->price;
        $percentage = (int) $validated['payment_percentage'];

        // 整数运算,并四舍五入到最小货币单位。
        $amountDue = (int) round($servicePrice * $percentage / 100);

        $appointment = DB::transaction(function () use (
            $request,
            $service,
            $servicePrice,
            $percentage,
            $amountDue
        ) {
            return Appointment::create([
                'user_id' => $request->user()->id,
                'service_id' => $service->id,
                'service_price' => $servicePrice,
                'payment_percentage' => $percentage,
                'amount_due' => $amountDue,
                'amount_paid' => 0,
                'payment_status' => 'pending',
            ]);
        });

        // 下一步应创建支付平台的支付请求。
        return redirect()->route('appointments.payment', $appointment);
    }
}

模型还要允许这些字段进行批量赋值:

// app/Models/Appointment.php

protected $fillable = [
    'user_id',
    'service_id',
    'service_price',
    'payment_percentage',
    'amount_due',
    'amount_paid',
    'payment_status',
    'payment_reference',
];

对应路由可以这样写:

use App\Http\Controllers\AppointmentController;
use App\Http\Controllers\AppointmentPaymentController;
use Illuminate\Support\Facades\Route;

Route::middleware('auth')->group(function () {
    Route::post('/appointments', [
        AppointmentController::class,
        'store',
    ])->name('appointments.store');

    Route::get('/appointments/{appointment}/payment', [
        AppointmentPaymentController::class,
        'show',
    ])->name('appointments.payment');
});

金额计算示例

如果数据库使用欧分,50€ 应保存为 5000:

$servicePrice = 5000;
$percentage = 30;

$amountDue = (int) round($servicePrice * $percentage / 100);

// 结果为 1500,即 15.00€

在页面上展示时,可以转换格式:

{{ number_format($appointment->amount_due / 100, 2, ',', '.') }} €

接入实际支付平台

算出 amount_due 后,把这个金额传给 Stripe、PayPal 或其他支付平台。具体 API 取决于使用的支付服务,基本流程如下:

  1. 创建一条状态为 pending 的预约。
  2. 用 amount_due 创建支付订单。
  3. 把支付平台返回的订单编号保存到 payment_reference。
  4. 用户在支付平台完成付款。
  5. 支付平台通过 webhook 通知 Laravel。
  6. Laravel 验证 webhook 签名和实际支付金额。
  7. 验证通过后,更新 amount_paid 和 payment_status。

用户付款后跳转到“成功页面”,并不能证明支付已经完成。浏览器跳转可能被伪造,也可能中断。付款结果应以支付平台经过签名验证的 webhook 为准。

更新逻辑可以参考下面的写法:

DB::transaction(function () use ($appointment, $paidAmount) {
    $appointment->amount_paid = $paidAmount;

    if ($paidAmount >= $appointment->service_price) {
        $appointment->payment_status = 'paid';
    } elseif ($paidAmount >= $appointment->amount_due) {
        $appointment->payment_status = 'deposit_paid';
    } else {
        $appointment->payment_status = 'partially_paid';
    }

    $appointment->save();
});

实际处理 webhook 时,还要核对支付金额:

if ($paidAmount !== $appointment->amount_due) {
    // 记录异常,不要自动确认预约。
}

后续尾款处理

定金到账后,剩余金额可以这样计算:

$remainingAmount = max(
    0,
    $appointment->service_price - $appointment->amount_paid
);

例如服务价格为 5000 分,已经支付 1500 分:

$remainingAmount = 5000 - 1500; // 3500,即 35.00€

支付尾款时,应创建一条新的支付记录。更完整的方案通常会增加 payments 表,不把所有付款信息都放在预约表中:

appointments
├── service_price
├── payment_status
└── payment_percentage

payments
├── appointment_id
├── amount
├── status
├── provider
└── provider_reference

采用这种结构后,定金、尾款、补款和退款等多笔交易都能分别记录。

注意事项

  • 最低定金为 30% 时,服务器必须拒绝 20% 的请求,不能只在页面上隐藏这个选项。
  • 不要接收前端直接提交的 amount_due 或服务价格。
  • 金额应使用整数分保存,不要使用 float 或 double。
  • 创建预约时要保存价格快照,避免服务改价影响历史预约。
  • 支付状态应由经过验证的支付平台 webhook 更新。
  • Webhook 可能重复发送,处理逻辑需要具备幂等性,避免同一笔付款被重复记账。
  • 如果允许管理员配置付款比例,服务器仍要检查该比例不低于最低值,并限制在 1 到 100 之间。
  • 税费、退款、取消预约以及定金是否可退,需要根据业务所在地的法律和支付平台规则另行设计。

备注:内容仅供参考。