用PHP生成订单:完整功能实现方法教程
在电商系统开发中,订单生成是核心功能之一。一个健壮的订单生成流程,需要保证订单号的唯一性、数据的完整性,以及在高并发场景下的可靠性。本文将从数据库设计、订单号生成策略、核心业务逻辑、事务处理与安全性四个层面,详细讲解如何使用PHP实现订单生成功能。
一、订单数据库表设计
在编写代码之前,先设计好订单相关的数据库表。订单表存储订单主信息,订单商品表存储订单中包含的每一个商品明细。以下是一个典型的订单表结构设计。
1.1 订单主表 (orders)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int(11) unsigned | 自增主键 |
| order_sn | varchar(64) | 订单号(唯一索引) |
| user_id | int(11) unsigned | 用户ID |
| total_amount | decimal(10,2) | 订单总金额 |
| pay_amount | decimal(10,2) | 实付金额 |
| order_status | tinyint(1) | 订单状态:0待支付 1已支付 2已发货 3已完成 4已取消 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
1.2 订单商品表 (order_items)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int(11) unsigned | 自增主键 |
| order_id | int(11) unsigned | 关联订单主表ID |
| goods_id | int(11) unsigned | 商品ID |
| goods_name | varchar(255) | 商品名称 |
| goods_price | decimal(10,2) | 商品单价 |
| buy_num | int(11) | 购买数量 |
-- 订单主表 CREATE TABLE `orders` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `order_sn` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号', `user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID', `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单总金额', `pay_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '实付金额', `order_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '订单状态:0待支付 1已支付 2已发货 3已完成 4已取消', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_sn` (`order_sn`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; -- 订单商品表 CREATE TABLE `order_items` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `order_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '订单ID', `goods_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '商品ID', `goods_name` varchar(255) NOT NULL DEFAULT '' COMMENT '商品名称', `goods_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '商品单价', `buy_num` int(11) NOT NULL DEFAULT '0' COMMENT '购买数量', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单商品表';
二、订单号生成策略
订单号需要具备全局唯一性,同时兼顾可读性和生成效率。常用的订单号生成策略包括:时间戳加随机数、雪花算法、以及基于数据库自增ID的混淆编码。下面给出一种基于时间戳与随机数的订单号生成方法,适合大多数中小型应用。
<?php
/**
* 生成唯一订单号
* 规则:年月日时分秒 + 微秒 + 随机数 + 用户ID后四位
* 保证在同一个毫秒内也不会重复
* @param int $userId 用户ID
* @return string 生成的订单号
*/
function generateOrderSn($userId = 0) {
// 获取当前时间,精确到毫秒
list($msec, $sec) = explode(' ', microtime());
$msec = substr(str_replace('0.', '', $msec), 0, 3);
$timeStr = date('YmdHis') . $msec;
// 生成4位随机数
$randNum = str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
// 用户ID后4位,不足4位补0
$userIdStr = str_pad(substr($userId, -4), 4, '0', STR_PAD_LEFT);
return $timeStr . $randNum . $userIdStr;
}
// 测试生成
$orderSn = generateOrderSn(10086);
echo $orderSn;
// 输出示例:202510141530451234567810086
?>三、核心订单生成逻辑
订单生成的核心流程包括:接收前端提交的商品数据、校验商品库存与价格、计算订单总金额、写入订单主表与订单商品表、扣减库存。整个过程必须使用数据库事务,确保数据的一致性。
3.1 订单生成函数
以下代码演示了完整的订单创建逻辑。代码中包含了参数校验、库存检查、金额计算以及事务提交。注意使用了PDO扩展操作数据库,所有SQL语句均使用预处理方式防止SQL注入。
<?php
/**
* 创建订单
* @param int $userId 用户ID
* @param array $goodsList 商品列表,格式:[['goods_id'=>1, 'num'=>2], ['goods_id'=>2, 'num'=>1]]
* @param float $discount 折扣金额(可选)
* @return array 返回结果,包含订单号或错误信息
*/
function createOrder($userId, $goodsList, $discount = 0.00) {
// 数据库连接(使用PDO示例)
$pdo = new PDO('mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4', 'root', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
try {
// 开启事务
$pdo->beginTransaction();
// 1. 校验商品并计算总价
$totalAmount = 0.00;
$orderItems = [];
foreach ($goodsList as $item) {
$goodsId = intval($item['goods_id']);
$buyNum = intval($item['num']);
if ($buyNum <= 0) {
throw new Exception('购买数量必须大于0');
}
// 查询商品信息(使用预处理语句)
$stmt = $pdo->prepare('SELECT id, goods_name, price, stock FROM goods WHERE id = ? AND status = 1 LIMIT 1');
$stmt->execute([$goodsId]);
$goods = $stmt->fetch();
if (!$goods) {
throw new Exception('商品不存在或已下架,商品ID:' . $goodsId);
}
// 检查库存
if ($goods['stock'] < $buyNum) {
throw new Exception('商品【' . $goods['goods_name'] . '】库存不足');
}
// 计算该商品小计
$amount = bcmul($goods['price'], $buyNum, 2);
$totalAmount = bcadd($totalAmount, $amount, 2);
// 组装订单商品数据
$orderItems[] = [
'goods_id' => $goods['id'],
'goods_name' => $goods['goods_name'],
'goods_price' => $goods['price'],
'buy_num' => $buyNum,
// 预先记录原始库存,后续扣减用
'_stock' => $goods['stock'],
];
}
// 2. 计算实付金额(总金额 - 折扣)
$payAmount = bcsub($totalAmount, $discount, 2);
if ($payAmount < 0) {
$payAmount = 0.00;
}
// 3. 生成订单号
$orderSn = generateOrderSn($userId);
// 4. 写入订单主表
$stmt = $pdo->prepare(
'INSERT INTO orders (order_sn, user_id, total_amount, pay_amount, order_status, create_time, update_time)
VALUES (?, ?, ?, ?, 0, NOW(), NOW())'
);
$stmt->execute([$orderSn, $userId, $totalAmount, $payAmount]);
$orderId = $pdo->lastInsertId();
// 5. 写入订单商品表并扣减库存
$stmtItem = $pdo->prepare(
'INSERT INTO order_items (order_id, goods_id, goods_name, goods_price, buy_num)
VALUES (?, ?, ?, ?, ?)'
);
$stmtStock = $pdo->prepare('UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ?');
foreach ($orderItems as $item) {
// 插入订单商品明细
$stmtItem->execute([
$orderId,
$item['goods_id'],
$item['goods_name'],
$item['goods_price'],
$item['buy_num']
]);
// 扣减库存(带条件更新,防止超卖)
$stmtStock->execute([$item['buy_num'], $item['goods_id'], $item['buy_num']]);
if ($stmtStock->rowCount() === 0) {
throw new Exception('商品【' . $item['goods_name'] . '】库存扣减失败,可能已被其他订单占用');
}
}
// 提交事务
$pdo->commit();
return [
'success' => true,
'order_sn' => $orderSn,
'order_id' => $orderId,
'pay_amount' => $payAmount,
];
} catch (Exception $e) {
// 回滚事务
$pdo->rollBack();
return [
'success' => false,
'message' => $e->getMessage(),
];
}
}
?>3.2 调用示例
下面展示前端提交订单时,后端如何调用上述订单生成函数。假设用户选择了两个商品,购买数量分别为2和1。
<?php
// 假设用户ID从session或token中获取
$userId = 10086;
// 模拟用户提交的商品数据(通常来自POST请求)
$goodsList = [
['goods_id' => 1, 'num' => 2],
['goods_id' => 2, 'num' => 1],
];
// 折扣金额(比如优惠券)
$discount = 5.00;
// 调用创建订单函数
$result = createOrder($userId, $goodsList, $discount);
if ($result['success']) {
echo '订单创建成功,订单号:' . $result['order_sn'];
echo ',实付金额:' . $result['pay_amount'] . '元';
} else {
echo '订单创建失败:' . $result['message'];
}
?>四、安全性考虑与优化
订单生成功能直接涉及资金和库存,开发时必须充分考虑安全性与并发处理能力。以下几点是实际项目中必须关注的要点。
4.1 防止重复提交
在前端点击提交后,后端应在短时间内对同一用户的重复请求进行拦截。常用的方案是使用Redis记录一个唯一令牌(token),提交订单时校验并删除令牌,实现防重放。
<?php
/**
* 生成订单防重令牌
* 前端提交订单时需携带此令牌
*/
function generateOrderToken($userId) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$token = md5(uniqid(mt_rand(), true));
// 令牌有效期5分钟
$redis->setex('order_token:' . $userId, 300, $token);
return $token;
}
/**
* 校验并销毁令牌
*/
function checkOrderToken($userId, $token) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = 'order_token:' . $userId;
$storedToken = $redis->get($key);
if ($storedToken === $token) {
$redis->del($key);
return true;
}
return false;
}
?>4.2 库存超卖防护
在高并发场景下,上述代码中的 UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ? 是关键。通过在SQL层面添加 stock >= ? 条件,保证只有库存充足时才执行扣减,配合数据库的行锁机制,可以有效防止超卖。
如果并发量极高,可以考虑引入Redis预扣库存的方式,再异步同步到数据库。后续我会单独写一篇关于Redis解决高并发库存扣减的文章。
4.3 参数校验与数据过滤
所有从客户端接收的数据都不能信任。在订单生成函数中,所有输入都经过了类型转换(intval、floatval)和预处理语句绑定,可以有效防止SQL注入。此外,商品价格应该以数据库中记录的为准,不能相信前端传递的价格。
<?php // 价格必须以数据库为准,防止篡改 // 前端即使传递了price参数,后端也忽略,只从数据库读取 // 示例:恶意用户自己改价格 // 正确做法:永远使用数据库中存储的价格 $goodsPrice = $goods['price']; // 从数据库获取 $amount = bcmul($goodsPrice, $buyNum, 2); ?>
五、完整流程总结
一个标准的PHP订单生成流程可以归纳为以下步骤:
- 接收并校验用户提交的商品列表与数量。
- 生成唯一订单号,避免重复。
- 开启数据库事务,保证数据一致性。
- 逐项查询商品信息,校验库存与价格。
- 计算订单总金额与实付金额。
- 写入订单主表,获取订单ID。
- 循环写入订单商品明细表,同时扣减库存。
- 提交事务,返回订单号给前端。
- 如果过程中发生任何异常,回滚事务并返回错误信息。
以上实现方法适用于大多数PHP电商系统的订单生成模块。如果在实际业务中需要处理预售、拼团、秒杀等更复杂的场景,可以在基础流程上扩展相应的逻辑。建议开发者根据自身项目的并发量和业务复杂程度,选择合适的订单号生成策略以及库存扣减方案。