PAYATHON 2026

如何为简单网站设置在线信用卡和借记卡支付?

支付小周

结论

对于只有三种产品且无需注册的简单网站,商品页和订单流程可以自行开发,但不要自行接收、保存或处理银行卡号、有效期和 CVV。

更稳妥的做法是选择提供托管收银台(Hosted Checkout)的支付服务商。网站先创建本地订单,再通过服务商的官方 SDK 或 API 创建支付会话,然后将用户跳转到由服务商托管的 HTTPS 支付页面。支付结果必须通过已验证签名的 webhook 确认,不能仅凭浏览器跳转后的“支付成功”页面判断。

这样不需要安装完整的电商系统,也能大幅缩小 PCI DSS 合规范围,降低银行卡数据泄露的风险。

推荐的支付流程

可以采用以下简单且可靠的流程:

  1. 用户在商品页选择产品并提交购买请求。
  2. PHP 后端根据产品 ID 从数据库读取价格,不接受浏览器提交的金额。
  3. 后端创建状态为 pending 的本地订单。
  4. 后端调用支付网关 API 创建支付会话。
  5. 用户跳转到支付服务商的托管收银台。
  6. 服务商完成银行卡验证和支付处理。
  7. 支付服务商向网站的 webhook 地址发送支付结果。
  8. 网站验证 webhook 签名,并将订单更新为 paid、failed 或其他状态。
  9. 用户返回结果页面。该页面只展示订单状态,不能直接决定订单是否已经付款。

支付服务商负责处理银行卡数据,网站负责商品、订单和履约。

如何选择支付网关

优先选择支持商户所在国家或地区、结算币种以及目标客户银行卡的正规支付服务商。可用服务商和费率会随地区及时间变化,应以服务商当前的官方文档和合同为准。

选择时应检查以下内容:

  • 是否提供 Hosted Checkout、Payment Link 或安全的嵌入式支付组件。
  • 是否有持续维护的 PHP SDK 和完整的 API 文档。
  • 是否支持 credit card、debit card 以及必要的 3-D Secure 验证。
  • 是否提供 webhook 签名验证机制。
  • 是否支持退款、部分退款、支付争议和交易查询。
  • 交易费、退款费、拒付费以及结算周期。
  • 是否提供测试环境或测试模式。
  • 商户注册要求、身份审核流程和行业限制。
  • 客户付款页面支持哪些语言和币种。
  • 是否明确说明 PCI DSS 的责任范围。

不要只看单笔手续费。实际维护中,文档质量、异常处理、退款能力和 webhook 的可靠性通常更值得关注。

数据库的最小设计

即使只有三种商品,也应使用服务端商品表或受控配置,不要信任前端传入的价格。

CREATE TABLE products (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    price_minor INT UNSIGNED NOT NULL,
    currency CHAR(3) NOT NULL,
    active BOOLEAN NOT NULL DEFAULT TRUE
);

CREATE TABLE orders (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    public_id CHAR(36) NOT NULL UNIQUE,
    product_id BIGINT UNSIGNED NOT NULL,
    amount_minor INT UNSIGNED NOT NULL,
    currency CHAR(3) NOT NULL,
    status VARCHAR(32) NOT NULL DEFAULT 'pending',
    gateway_reference VARCHAR(255) NULL UNIQUE,
    customer_email VARCHAR(320) NULL,
    created_at DATETIME NOT NULL,
    paid_at DATETIME NULL,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

price_minor 和 amount_minor 使用最小货币单位。例如,19.99 元可按业务币种规则存为 1999。金额不要用浮点数保存,因为浮点运算可能产生精度误差。

创建订单和支付会话

下面是一个与具体支付服务商无关的 PHP 结构示例。GatewayClient、方法名和参数仅用于说明架构,实际开发时必须替换为所选服务商的官方 SDK。

<?php

declare(strict_types=1);

session_start();

$productId = filter_input(INPUT_POST, 'product_id', FILTER_VALIDATE_INT);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);

if (!$productId || !$email) {
    http_response_code(400);
    exit('Invalid request');
}

$pdo = new PDO(
    'mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4',
    $_ENV['DB_USER'],
    $_ENV['DB_PASSWORD'],
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]
);

$stmt = $pdo->prepare(
    'SELECT id, name, price_minor, currency
     FROM products
     WHERE id = :id AND active = 1'
);
$stmt->execute(['id' => $productId]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$product) {
    http_response_code(404);
    exit('Product not found');
}

$orderId = generateUuidV4();

$stmt = $pdo->prepare(
    'INSERT INTO orders
        (public_id, product_id, amount_minor, currency, status, customer_email, created_at)
     VALUES
        (:public_id, :product_id, :amount_minor, :currency, :status, :email, UTC_TIMESTAMP())'
);

$stmt->execute([
    'public_id'   => $orderId,
    'product_id'  => $product['id'],
    'amount_minor'=> $product['price_minor'],
    'currency'    => $product['currency'],
    'status'      => 'pending',
    'email'       => $email,
]);

$gateway = new GatewayClient($_ENV['PAYMENT_API_KEY']);

$checkout = $gateway->createCheckoutSession([
    'amount'      => (int) $product['price_minor'],
    'currency'    => $product['currency'],
    'description' => $product['name'],
    'reference'   => $orderId,
    'success_url' => 'https://example.com/payment/return?order=' .
                     rawurlencode($orderId),
    'cancel_url'  => 'https://example.com/checkout/cancelled',
]);

header('Location: ' . $checkout->url, true, 303);
exit;

generateUuidV4() 可以由成熟的 UUID 库提供。生产环境中不要自行拼凑随机订单号,也不要直接把数据库自增 ID 当作公开订单凭证。

API 密钥应存放在服务器环境变量或专用密钥管理系统中,不能写入 Git 仓库、前端 JavaScript 或公开配置文件。

正确处理 webhook

用户支付成功后可能直接关闭页面,也可能伪造对 success_url 的访问。因此,订单状态必须以经过验证的服务端通知为准。

<?php

declare(strict_types=1);

$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '';

try {
    $event = GatewayWebhook::verify(
        $rawBody,
        $signature,
        $_ENV['PAYMENT_WEBHOOK_SECRET']
    );
} catch (Throwable $e) {
    http_response_code(400);
    exit('Invalid webhook');
}

if ($event->type === 'payment.succeeded') {
    $orderId = $event->reference;
    $gatewayReference = $event->paymentId;

    $pdo->beginTransaction();

    $stmt = $pdo->prepare(
        'SELECT amount_minor, currency, status
         FROM orders
         WHERE public_id = :id
         FOR UPDATE'
    );
    $stmt->execute(['id' => $orderId]);
    $order = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$order) {
        $pdo->rollBack();
        http_response_code(404);
        exit;
    }

    if (
        (int) $order['amount_minor'] !== (int) $event->amount ||
        $order['currency'] !== $event->currency
    ) {
        $pdo->rollBack();
        http_response_code(400);
        exit('Payment mismatch');
    }

    if ($order['status'] !== 'paid') {
        $stmt = $pdo->prepare(
            'UPDATE orders
             SET status = :status,
                 gateway_reference = :gateway_reference,
                 paid_at = UTC_TIMESTAMP()
             WHERE public_id = :id'
        );

        $stmt->execute([
            'status'            => 'paid',
            'gateway_reference' => $gatewayReference,
            'id'                => $orderId,
        ]);
    }

    $pdo->commit();
}

http_response_code(200);

各支付服务商使用的签名请求头、事件字段和验证方法并不相同,必须使用对应的官方 SDK,不能将示例中的名称原样用于生产环境。

webhook 处理还需要具备幂等性。同一事件被重复发送时,系统不能重复发货、重复发送兑换码或重复记账。

HTTPS 证书如何配置

整个网站都应使用 HTTPS,而不仅是付款页面。证书可以从受信任的证书颁发机构获取。许多服务器会使用 Let’s Encrypt,并通过 Certbot 或托管平台自动签发和续期。

以 Nginx 为例,HTTPS 配置通常至少包括:

server {
    listen 80;
    server_name example.com www.example.com;

    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /path/to/fullchain.pem;
    ssl_certificate_key /path/to/privkey.pem;

    root /var/www/shop/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

证书路径和 TLS 配置取决于发行版、Web 服务器及证书工具,不能直接照搬示例。部署后应确认:

  • HTTP 请求会跳转到 HTTPS。
  • 证书域名与实际域名一致。
  • 证书链完整,并且证书能够自动续期。
  • 页面没有通过 HTTP 加载图片、脚本或样式。
  • 只有必要的系统账户可以读取私钥。
  • 服务器时间准确,否则证书和 webhook 验证可能出现异常。

确认所有子域都支持 HTTPS 后,可以考虑启用 HSTS,但启用前需要了解其长期缓存带来的影响。

必须避免的做法

除非已经具备相应的 PCI DSS 合规能力和专业安全审计条件,否则不要在自己的表单中收集完整卡号、CVV 或磁条数据。尤其不要:

  • 将卡号或 CVV 保存到 MySQL。
  • 把卡片信息写入日志、邮件或错误报告。
  • 通过普通 PHP 表单接收卡片数据,再转发给网关。
  • 根据前端传入的价格创建交易。
  • 仅凭返回页面判断付款成功。
  • 在 URL 中放置 API 密钥、客户隐私数据或可猜测的订单凭证。
  • 因为网站规模小就忽略退款、重复通知和支付争议。

即使采用托管收银台,网站仍需做好输入验证、参数化查询、访问控制、依赖更新和安全日志记录。

上线前检查

正式收款前,至少测试以下情况:

  • 正常付款、付款失败和用户取消。
  • 3-D Secure 或其他银行卡验证流程。
  • 用户付款后直接关闭浏览器。
  • webhook 延迟、重复发送和乱序到达。
  • 金额或币种不一致时拒绝履约。
  • 刷新结果页不会导致重复发货。
  • 退款后订单状态能够同步。
  • 数据库或邮件服务短暂失败后的恢复处理。
  • 测试密钥与生产密钥严格分离。
  • 日志中不含银行卡数据、API 密钥或 webhook secret。

只有三种商品,不代表一定要使用完整的商城系统。自行开发轻量订单页面是合理的,但银行卡处理应交给成熟的支付服务商。将自建部分限制在商品选择、订单记录、状态展示和履约,通常能在成本、安全性和维护难度之间取得更合适的平衡。

备注:内容仅供参考。