如何为简单网站设置在线信用卡和借记卡支付?
结论
对于只有三种产品且无需注册的简单网站,商品页和订单流程可以自行开发,但不要自行接收、保存或处理银行卡号、有效期和 CVV。
更稳妥的做法是选择提供托管收银台(Hosted Checkout)的支付服务商。网站先创建本地订单,再通过服务商的官方 SDK 或 API 创建支付会话,然后将用户跳转到由服务商托管的 HTTPS 支付页面。支付结果必须通过已验证签名的 webhook 确认,不能仅凭浏览器跳转后的“支付成功”页面判断。
这样不需要安装完整的电商系统,也能大幅缩小 PCI DSS 合规范围,降低银行卡数据泄露的风险。
推荐的支付流程
可以采用以下简单且可靠的流程:
- 用户在商品页选择产品并提交购买请求。
- PHP 后端根据产品 ID 从数据库读取价格,不接受浏览器提交的金额。
- 后端创建状态为
pending的本地订单。 - 后端调用支付网关 API 创建支付会话。
- 用户跳转到支付服务商的托管收银台。
- 服务商完成银行卡验证和支付处理。
- 支付服务商向网站的 webhook 地址发送支付结果。
- 网站验证 webhook 签名,并将订单更新为
paid、failed或其他状态。 - 用户返回结果页面。该页面只展示订单状态,不能直接决定订单是否已经付款。
支付服务商负责处理银行卡数据,网站负责商品、订单和履约。
如何选择支付网关
优先选择支持商户所在国家或地区、结算币种以及目标客户银行卡的正规支付服务商。可用服务商和费率会随地区及时间变化,应以服务商当前的官方文档和合同为准。
选择时应检查以下内容:
- 是否提供 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。
只有三种商品,不代表一定要使用完整的商城系统。自行开发轻量订单页面是合理的,但银行卡处理应交给成熟的支付服务商。将自建部分限制在商品选择、订单记录、状态展示和履约,通常能在成本、安全性和维护难度之间取得更合适的平衡。
备注:内容仅供参考。
