实战 PHP 电商开发:从零构建完整在线商店-3
本章介绍了电商系统核心的购物车与订单处理流程实现。主要内容包括:使用会话存储实现购物车数据持久化,通过AJAX技术实现购物车异步更新操作;设计并实现订单状态机管理订单流转逻辑;开发库存管理系统确保数据一致性;以及集成邮件通知系统。本章作为电商平台交易环节的关键部分,连接了前端用户交互与后端业务处理,涵盖了从购物车管理到订单完成的完整交易流程,并提供了PHP实现的代码示例和安全设计考虑。
第 3 章 购物车系统与订单处理流程实现
章节介绍
学习目标:
- 掌握使用会话存储实现购物车数据持久化
- 能够使用 AJAX 实现购物车的异步更新操作
- 理解并实现订单状态机的设计与流转逻辑
- 开发完整的库存管理系统
- 设计订单详情页面和邮件通知系统
在整个教程中的作用:
本章是电商系统的核心交易环节,连接了前端的用户交互与后端的业务逻辑处理。购物车和订单系统直接关系到用户体验和商家的运营效率,是电商平台能否成功的关键因素。
与前面章节的衔接: - 使用第 2 章开发的用户认证系统来关联购物车和订单数据
- 基于第 2 章的产品管理模块获取商品信息和库存数据
- 在第 1 章建立的 MVC 框架基础上扩展购物车和订单相关功能
本章主要内容概览:
- 购物车系统的设计与实现
- AJAX 异步操作优化用户体验
- 订单生成与状态管理
- 库存实时检查与更新
- 邮件通知系统集成
核心概念讲解
1. 会话存储购物车数据持久化
技术原理:
会话(Session)是服务器端存储用户状态信息的机制,通过 Session ID 与客户端 Cookie 关联。购物车数据存储在 Session 中,可以保证用户在同一浏览器会话期间购物车内容的持续性。
应用场景:
- 用户添加商品到购物车,数据临时保存在 Session 中
- 用户刷新页面或跳转页面时,购物车数据不丢失
- 用户登录后,可以将 Session 中的购物车数据与用户账户关联
注意事项: - Session 数据在用户关闭浏览器后可能丢失,重要数据需要持久化到数据库
- 购物车数据量不宜过大,避免影响服务器性能
- 需要考虑 Session 共享问题,在集群环境中使用 Redis 等共享存储
最佳实践:
// Session配置优化
ini_set('session.gc_maxlifetime', 3600); // 设置Session过期时间
session_set_cookie_params(3600, '/', '.yourdomain.com', true, true); // 安全配置
2. AJAX 异步更新购物车数量
技术原理:
AJAX(Asynchronous JavaScript and XML)允许网页在不重新加载的情况下与服务器交换数据。通过 XMLHttpRequest 或 Fetch API 实现异步通信。
应用场景:
- 实时更新购物车商品数量
- 动态计算总价和运费
- 提供流畅的用户交互体验
注意事项: - 需要处理网络异常和服务器错误
- 考虑并发操作的数据一致性
- 提供适当的加载状态提示
3. 订单状态机设计与实现
技术原理:
订单状态机定义了订单从创建到完成的所有可能状态及其转换规则。每个状态转换都有特定的前置条件和后置动作。
典型状态流转:
待支付 → 已支付 → 已发货 → 已完成
↘
已取消
设计考虑:
- 状态转换应该是原子操作
- 记录状态变更历史用于审计
- 考虑异常状态的处理(如退款、部分发货)
4. 库存管理系统开发
技术原理:
库存管理确保商品可售数量与实际物理库存一致。需要处理高并发下的库存扣减,防止超卖。
关键技术:
- 数据库行级锁或乐观锁
- Redis 原子操作
- 消息队列异步处理
库存扣减策略:
// 悲观锁实现
BEGIN TRANSACTION;
SELECT stock FROM products WHERE id = ? FOR UPDATE;
UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?;
COMMIT;
5. 邮件通知系统集成
技术原理:
使用 SMTP 协议或第三方邮件服务 API 发送交易通知邮件。邮件内容通常使用模板引擎生成。
关键邮件类型:
- 订单确认邮件
- 支付成功通知
- 发货通知
- 订单完成通知
代码示例
示例 1:购物车核心类实现
<?php
/**
* 购物车管理类
* 负责购物车的增删改查和持久化操作
*/
class CartManager
{
private $sessionKey = 'shopping_cart';
/**
* 添加商品到购物车
* @param int $productId 商品ID
* @param int $quantity 数量
* @param array $productInfo 商品信息
* @return bool 添加结果
*/
public function addToCart($productId, $quantity, $productInfo = [])
{
// 启动Session
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// 参数验证
if ($productId <= 0 || $quantity <= 0) {
throw new InvalidArgumentException('商品ID和数量必须大于0');
}
// 初始化购物车
if (!isset($_SESSION[$this->sessionKey])) {
$_SESSION[$this->sessionKey] = [];
}
// 检查商品是否已存在
if (isset($_SESSION[$this->sessionKey][$productId])) {
// 已存在,增加数量
$_SESSION[$this->sessionKey][$productId]['quantity'] += $quantity;
} else {
// 新商品,添加到购物车
$_SESSION[$this->sessionKey][$productId] = [
'quantity' => $quantity,
'product_info' => $productInfo,
'added_time' => time()
];
}
return true;
}
/**
* 从购物车移除商品
* @param int $productId 商品ID
* @return bool 移除结果
*/
public function removeFromCart($productId)
{
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (isset($_SESSION[$this->sessionKey][$productId])) {
unset($_SESSION[$this->sessionKey][$productId]);
return true;
}
return false;
}
/**
* 更新购物车商品数量
* @param int $productId 商品ID
* @param int $quantity 新数量
* @return bool 更新结果
*/
public function updateQuantity($productId, $quantity)
{
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if ($quantity <= 0) {
// 数量为0或负数,移除商品
return $this->removeFromCart($productId);
}
if (isset($_SESSION[$this->sessionKey][$productId])) {
$_SESSION[$this->sessionKey][$productId]['quantity'] = $quantity;
return true;
}
return false;
}
/**
* 获取购物车内容
* @return array 购物车数据
*/
public function getCartContents()
{
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
return isset($_SESSION[$this->sessionKey]) ? $_SESSION[$this->sessionKey] : [];
}
/**
* 清空购物车
* @return bool 清空结果
*/
public function clearCart()
{
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
unset($_SESSION[$this->sessionKey]);
return true;
}
/**
* 获取购物车商品总数
* @return int 商品总数量
*/
public function getTotalItems()
{
$contents = $this->getCartContents();
$total = 0;
foreach ($contents as $item) {
$total += $item['quantity'];
}
return $total;
}
/**
* 计算购物车总金额
* @return float 总金额
*/
public function getTotalAmount()
{
$contents = $this->getCartContents();
$total = 0;
foreach ($contents as $item) {
if (isset($item['product_info']['price'])) {
$total += $item['product_info']['price'] * $item['quantity'];
}
}
return $total;
}
}
?>
示例 2:AJAX 购物车操作接口
<?php
/**
* 购物车AJAX接口控制器
*/
class CartApiController
{
private $cartManager;
private $productModel;
public function __construct()
{
$this->cartManager = new CartManager();
$this->productModel = new ProductModel();
}
/**
* 添加商品到购物车
*/
public function addItem()
{
header('Content-Type: application/json');
try {
// 验证请求方法
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('只支持POST请求');
}
// 获取并验证参数
$productId = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
$quantity = isset($_POST['quantity']) ? intval($_POST['quantity']) : 1;
if ($productId <= 0) {
throw new Exception('商品ID无效');
}
// 获取商品信息
$productInfo = $this->productModel->getProductById($productId);
if (!$productInfo) {
throw new Exception('商品不存在');
}
// 检查库存
if ($productInfo['stock'] < $quantity) {
throw new Exception('库存不足,当前库存:' . $productInfo['stock']);
}
// 添加到购物车
$result = $this->cartManager->addToCart($productId, $quantity, [
'name' => $productInfo['name'],
'price' => $productInfo['price'],
'image' => $productInfo['image']
]);
if ($result) {
echo json_encode([
'success' => true,
'message' => '添加成功',
'data' => [
'total_items' => $this->cartManager->getTotalItems(),
'total_amount' => $this->cartManager->getTotalAmount()
]
]);
} else {
throw new Exception('添加失败');
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
}
/**
* 更新购物车商品数量
*/
public function updateQuantity()
{
header('Content-Type: application/json');
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('只支持POST请求');
}
$productId = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
$quantity = isset($_POST['quantity']) ? intval($_POST['quantity']) : 0;
if ($productId <= 0) {
throw new Exception('商品ID无效');
}
// 检查商品是否存在
$productInfo = $this->productModel->getProductById($productId);
if (!$productInfo) {
throw new Exception('商品不存在');
}
// 检查库存
if ($quantity > 0 && $productInfo['stock'] < $quantity) {
throw new Exception('库存不足,最大可购买:' . $productInfo['stock']);
}
$result = $this->cartManager->updateQuantity($productId, $quantity);
if ($result) {
echo json_encode([
'success' => true,
'message' => '更新成功',
'data' => [
'total_items' => $this->cartManager->getTotalItems(),
'total_amount' => $this->cartManager->getTotalAmount()
]
]);
} else {
throw new Exception('更新失败');
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
}
/**
* 获取购物车信息
*/
public function getCartInfo()
{
header('Content-Type: application/json');
$cartContents = $this->cartManager->getCartContents();
$formattedContents = [];
foreach ($cartContents as $productId => $item) {
$formattedContents[] = [
'product_id' => $productId,
'quantity' => $item['quantity'],
'product_info' => $item['product_info']
];
}
echo json_encode([
'success' => true,
'data' => [
'items' => $formattedContents,
'total_items' => $this->cartManager->getTotalItems(),
'total_amount' => $this->cartManager->getTotalAmount()
]
]);
}
}
// 使用示例:路由分发
$action = isset($_GET['action']) ? $_GET['action'] : '';
$controller = new CartApiController();
switch ($action) {
case 'add':
$controller->addItem();
break;
case 'update':
$controller->updateQuantity();
break;
case 'info':
$controller->getCartInfo();
break;
default:
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '未知操作']);
}
?>
示例 3:订单状态机实现
<?php
/**
* 订单状态机类
* 管理订单状态的转换和验证
*/
class OrderStateMachine
{
// 订单状态常量定义
const STATE_PENDING = 'pending'; // 待支付
const STATE_PAID = 'paid'; // 已支付
const STATE_SHIPPED = 'shipped'; // 已发货
const STATE_COMPLETED = 'completed'; // 已完成
const STATE_CANCELLED = 'cancelled'; // 已取消
const STATE_REFUNDED = 'refunded'; // 已退款
// 状态转换规则定义
private static $transitions = [
self::STATE_PENDING => [
self::STATE_PAID, // 待支付 → 已支付
self::STATE_CANCELLED // 待支付 → 已取消
],
self::STATE_PAID => [
self::STATE_SHIPPED, // 已支付 → 已发货
self::STATE_REFUNDED // 已支付 → 已退款
],
self::STATE_SHIPPED => [
self::STATE_COMPLETED, // 已发货 → 已完成
self::STATE_REFUNDED // 已发货 → 已退款
],
self::STATE_COMPLETED => [
// 已完成是终态,不能转换到其他状态
],
self::STATE_CANCELLED => [
// 已取消是终态,不能转换到其他状态
],
self::STATE_REFUNDED => [
// 已退款是终态,不能转换到其他状态
]
];
/**
* 检查状态转换是否允许
* @param string $fromState 原状态
* @param string $toState 目标状态
* @return bool 是否允许转换
*/
public static function canTransition($fromState, $toState)
{
if (!isset(self::$transitions[$fromState])) {
return false;
}
return in_array($toState, self::$transitions[$fromState]);
}
/**
* 执行状态转换
* @param Order $order 订单对象
* @param string $newState 新状态
* @param string $remark 状态变更备注
* @return bool 转换结果
*/
public static function transition(Order $order, $newState, $remark = '')
{
$currentState = $order->getState();
if (!self::canTransition($currentState, $newState)) {
throw new InvalidArgumentException(
"不允许从状态 {$currentState} 转换到 {$newState}"
);
}
// 执行状态转换前的验证和操作
if (!self::beforeTransition($order, $newState)) {
return false;
}
// 更新订单状态
$order->setState($newState);
// 记录状态变更历史
self::logStateChange($order, $currentState, $newState, $remark);
// 执行状态转换后的操作
self::afterTransition($order, $newState);
return true;
}
/**
* 状态转换前的验证和操作
*/
private static function beforeTransition(Order $order, $newState)
{
switch ($newState) {
case self::STATE_PAID:
// 支付前的验证,如检查库存等
return self::beforePaid($order);
case self::STATE_CANCELLED:
// 取消前的验证
return self::beforeCancelled($order);
case self::STATE_REFUNDED:
// 退款前的验证
return self::beforeRefunded($order);
default:
return true;
}
}
/**
* 状态转换后的操作
*/
private static function afterTransition(Order $order, $newState)
{
switch ($newState) {
case self::STATE_PAID:
// 支付成功后的操作,如扣减库存、发送邮件等
self::afterPaid($order);
break;
case self::STATE_CANCELLED:
// 取消订单后的操作,如恢复库存
self::afterCancelled($order);
break;
case self::STATE_SHIPPED:
// 发货后的操作,如发送发货通知
self::afterShipped($order);
break;
}
}
/**
* 支付前的验证
*/
private static function beforePaid(Order $order)
{
// 检查库存
foreach ($order->getItems() as $item) {
$product = Product::find($item->product_id);
if ($product->stock < $item->quantity) {
throw new Exception("商品 {$product->name} 库存不足");
}
}
return true;
}
/**
* 支付成功后的操作
*/
private static function afterPaid(Order $order)
{
// 扣减库存
foreach ($order->getItems() as $item) {
Product::where('id', $item->product_id)
->decrement('stock', $item->quantity);
}
// 发送支付成功邮件
MailService::sendOrderPaidEmail($order);
// 记录日志
Logger::info("订单 {$order->id} 支付成功");
}
/**
* 记录状态变更历史
*/
private static function logStateChange($order, $fromState, $toState, $remark)
{
OrderStateLog::create([
'order_id' => $order->id,
'from_state' => $fromState,
'to_state' => $toState,
'remark' => $remark,
'operator' => Auth::user() ? Auth::user()->id : 'system',
'created_at' => date('Y-m-d H:i:s')
]);
}
/**
* 获取所有可能的状态
*/
public static function getAllStates()
{
return array_keys(self::$transitions);
}
/**
* 获取从当前状态可以转换到的状态
*/
public static function getNextStates($currentState)
{
return isset(self::$transitions[$currentState])
? self::$transitions[$currentState]
: [];
}
}
/**
* 订单状态日志模型
*/
class OrderStateLog extends Model
{
protected $table = 'order_state_logs';
protected $fillable = [
'order_id', 'from_state', 'to_state', 'remark', 'operator'
];
}
?>
示例 4:库存管理服务
<?php
/**
* 库存管理服务
* 负责库存的检查、扣减和恢复
*/
class InventoryService
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* 检查库存是否充足
* @param int $productId 商品ID
* @param int $requiredQuantity 需求数量
* @return bool 库存是否充足
*/
public function checkStock($productId, $requiredQuantity)
{
$stmt = $this->db->prepare("
SELECT stock FROM products
WHERE id = ? AND status = 'active'
");
$stmt->execute([$productId]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) {
throw new Exception("商品不存在或已下架");
}
return $product['stock'] >= $requiredQuantity;
}
/**
* 批量检查库存
* @param array $items 商品列表 [product_id => quantity]
* @return array 检查结果
*/
public function batchCheckStock($items)
{
$results = [];
$productIds = array_keys($items);
if (empty($productIds)) {
return $results;
}
$placeholders = str_repeat('?,', count($productIds) - 1) . '?';
$stmt = $this->db->prepare("
SELECT id, name, stock FROM products
WHERE id IN ($placeholders) AND status = 'active'
");
$stmt->execute($productIds);
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
$productMap = [];
foreach ($products as $product) {
$productMap[$product['id']] = $product;
}
foreach ($items as $productId => $quantity) {
if (!isset($productMap[$productId])) {
$results[$productId] = [
'success' => false,
'message' => '商品不存在或已下架'
];
continue;
}
$product = $productMap[$productId];
if ($product['stock'] < $quantity) {
$results[$productId] = [
'success' => false,
'message' => "商品 {$product['name']} 库存不足,当前库存:{$product['stock']}"
];
} else {
$results[$productId] = [
'success' => true,
'message' => '库存充足'
];
}
}
return $results;
}
/**
* 扣减库存(使用悲观锁防止超卖)
* @param int $productId 商品ID
* @param int $quantity 扣减数量
* @return bool 扣减结果
*/
public function deductStock($productId, $quantity)
{
$this->db->beginTransaction();
try {
// 使用行级锁
$stmt = $this->db->prepare("
SELECT stock FROM products
WHERE id = ? FOR UPDATE
");
$stmt->execute([$productId]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) {
throw new Exception("商品不存在");
}
if ($product['stock'] < $quantity) {
throw new Exception("库存不足,当前库存:{$product['stock']}");
}
// 扣减库存
$updateStmt = $this->db->prepare("
UPDATE products SET stock = stock - ?
WHERE id = ? AND stock >= ?
");
$updateStmt->execute([$quantity, $productId, $quantity]);
if ($updateStmt->rowCount() === 0) {
throw new Exception("库存扣减失败");
}
// 记录库存变更日志
$this->logStockChange($productId, -$quantity, 'order_deduct');
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
/**
* 恢复库存
* @param int $productId 商品ID
* @param int $quantity 恢复数量
* @return bool 恢复结果
*/
public function restoreStock($productId, $quantity)
{
$stmt = $this->db->prepare("
UPDATE products SET stock = stock + ?
WHERE id = ?
");
$result = $stmt->execute([$quantity, $productId]);
if ($result) {
$this->logStockChange($productId, $quantity, 'order_restore');
}
return $result;
}
/**
* 批量扣减库存
* @param array $items 商品列表 [product_id => quantity]
* @return array 扣减结果
*/
public function batchDeductStock($items)
{
$results = [];
$this->db->beginTransaction();
try {
foreach ($items as $productId => $quantity) {
try {
$results[$productId] = [
'success' => $this->deductStock($productId, $quantity),
'message' => '库存扣减成功'
];
} catch (Exception $e) {
$results[$productId] = [
'success' => false,
'message' => $e->getMessage()
];
// 有一个失败就回滚整个事务
throw $e;
}
}
$this->db->commit();
return $results;
} catch (Exception $e) {
$this->db->rollBack();
// 返回所有商品的扣减结果
foreach ($items as $productId => $quantity) {
if (!isset($results[$productId])) {
$results[$productId] = [
'success' => false,
'message' => '批量扣减失败:' . $e->getMessage()
];
}
}
return $results;
}
}
/**
* 记录库存变更日志
*/
private function logStockChange($productId, $change, $type)
{
$stmt = $this->db->prepare("
INSERT INTO stock_change_log
(product_id, change_amount, change_type, created_at)
VALUES (?, ?, ?, NOW())
");
$stmt->execute([$productId, $change, $type]);
}
}
// 使用Redis实现高并发库存控制
class RedisInventoryService
{
private $redis;
public function __construct($redis)
{
$this->redis = $redis;
}
/**
* 使用Redis原子操作扣减库存
*/
public function deductStockWithRedis($productId, $quantity)
{
$key = "product_stock:{$productId}";
// 使用Lua脚本保证原子性
$lua = "
local stock = tonumber(redis.call('get', KEYS[1]))
local quantity = tonumber(ARGV[1])
if stock == nil then
return -1 -- 商品不存在
end
if stock < quantity then
return -2 -- 库存不足
end
redis.call('decrby', KEYS[1], quantity)
return stock - quantity
";
$result = $this->redis->eval($lua, [$key, $quantity], 1);
switch ($result) {
case -1:
throw new Exception("商品不存在");
case -2:
throw new Exception("库存不足");
default:
return $result;
}
}
}
?>
示例 5:邮件通知系统
<?php
/**
* 邮件通知服务
*/
class MailService
{
private $smtpHost;
private $smtpPort;
private $username;
private $password;
private $fromAddress;
public function __construct($config)
{
$this->smtpHost = $config['smtp_host'];
$this->smtpPort = $config['smtp_port'];
$this->username = $config['username'];
$this->password = $config['password'];
$this->fromAddress = $config['from_address'];
}
/**
* 发送订单确认邮件
* @param Order $order 订单对象
* @param string $toEmail 收件人邮箱
* @return bool 发送结果
*/
public function sendOrderConfirmation($order, $toEmail)
{
$subject = "订单确认 - 订单号: {$order->order_number}";
$body = $this->renderTemplate('order_confirmation', [
'order' => $order,
'customer' => $order->customer,
'items' => $order->items
]);
return $this->send($toEmail, $subject, $body);
}
/**
* 发送支付成功邮件
* @param Order $order 订单对象
* @param string $toEmail 收件人邮箱
* @return bool 发送结果
*/
public function sendPaymentSuccess($order, $toEmail)
{
$subject = "支付成功 - 订单号: {$order->order_number}";
$body = $this->renderTemplate('payment_success', [
'order' => $order,
'payment' => $order->payment
]);
return $this->send($toEmail, $subject, $body);
}
/**
* 发送发货通知邮件
* @param Order $order 订单对象
* @param string $toEmail 收件人邮箱
* @return bool 发送结果
*/
public function sendShippingNotification($order, $toEmail)
{
$subject = "您的订单已发货 - 订单号: {$order->order_number}";
$body = $this->renderTemplate('shipping_notification', [
'order' => $order,
'shipping' => $order->shipping
]);
return $this->send($toEmail, $subject, $body);
}
/**
* 通用邮件发送方法
*/
private function send($to, $subject, $body)
{
// 使用PHPMailer或其他邮件库
$mail = new PHPMailer(true);
try {
// 服务器配置
$mail->isSMTP();
$mail->Host = $this->smtpHost;
$mail->SMTPAuth = true;
$mail->Username = $this->username;
$mail->Password = $this->password;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = $this->smtpPort;
// 收件人
$mail->setFrom($this->fromAddress, '电商平台');
$mail->addAddress($to);
// 内容
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->AltBody = strip_tags($body); // 纯文本版本
return $mail->send();
} catch (Exception $e) {
error_log("邮件发送失败: {$mail->ErrorInfo}");
return false;
}
}
/**
* 渲染邮件模板
*/
private function renderTemplate($template, $data)
{
extract($data);
ob_start();
include __DIR__ . "/templates/email/{$template}.php";
return ob_get_clean();
}
}
// 邮件模板示例:order_confirmation.php
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>订单确认</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #f8f9fa; padding: 20px; text-align: center; }
.order-info { background: #e9ecef; padding: 15px; margin: 20px 0; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.table th { background-color: #f2f2f2; }
.total { font-weight: bold; font-size: 1.2em; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>订单确认</h1>
<p>感谢您在我们的商店购物!</p>
</div>
<div class="order-info">
<h2>订单信息</h2>
<p><strong>订单号:</strong> <?= $order->order_number ?></p>
<p><strong>下单时间:</strong> <?= $order->created_at ?></p>
<p><strong>订单状态:</strong> <?= $order->getStateText() ?></p>
</div>
<h2>订单明细</h2>
<table class="table">
<thead>
<tr>
<th>商品名称</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<tr>
<td><?= htmlspecialchars($item->product_name) ?></td>
<td>¥<?= number_format($item->price, 2) ?></td>
<td><?= $item->quantity ?></td>
<td>¥<?= number_format($item->price * $item->quantity, 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="total">总计:</td>
<td class="total">¥<?= number_format($order->total_amount, 2) ?></td>
</tr>
</tfoot>
</table>
<div class="shipping-info">
<h2>配送信息</h2>
<p><strong>收货人:</strong> <?= htmlspecialchars($order->shipping_name) ?></p>
<p><strong>联系电话:</strong> <?= htmlspecialchars($order->shipping_phone) ?></p>
<p><strong>收货地址:</strong> <?= htmlspecialchars($order->shipping_address) ?></p>
</div>
<div class="footer">
<p>如有任何问题,请联系我们的客服。</p>
<p>谢谢!<br>电商平台团队</p>
</div>
</div>
</body>
</html>
实战项目
项目需求:开发完整的购物车和订单处理系统
项目描述:
构建一个完整的电商购物车和订单处理系统,支持商品添加、数量修改、库存检查、订单生成、状态管理和邮件通知等完整业务流程。
技术方案:
- 使用 Session 存储购物车数据,支持用户登录后数据同步
- 采用 AJAX 实现购物车的异步操作,提升用户体验
- 实现订单状态机管理订单生命周期
- 集成库存管理系统防止超卖
- 使用邮件服务发送交易通知
分步骤实现:
步骤 1:数据库设计
-- 购物车表(用户登录后持久化)
CREATE TABLE cart (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id),
UNIQUE KEY unique_user_product (user_id, product_id)
);
-- 订单表
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_number VARCHAR(50) UNIQUE NOT NULL,
user_id INT NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
shipping_name VARCHAR(100) NOT NULL,
shipping_phone VARCHAR(20) NOT NULL,
shipping_address TEXT NOT NULL,
payment_method VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 订单商品表
CREATE TABLE order_items (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
product_id INT NOT NULL,
product_name VARCHAR(255) NOT NULL,
product_price DECIMAL(10,2) NOT NULL,
quantity INT NOT NULL,
subtotal DECIMAL(10,2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id)
);
-- 订单状态日志表
CREATE TABLE order_state_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
from_state VARCHAR(20) NOT NULL,
to_state VARCHAR(20) NOT NULL,
remark TEXT,
operator VARCHAR(50) DEFAULT 'system',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
-- 库存变更日志表
CREATE TABLE stock_change_log (
id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT NOT NULL,
change_amount INT NOT NULL,
change_type VARCHAR(50) NOT NULL,
reference_id INT, -- 关联订单ID等
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id)
);
步骤 2:购物车控制器实现
<?php
class CartController
{
private $cartManager;
private $productModel;
private $inventoryService;
public function __construct()
{
$this->cartManager = new CartManager();
$this->productModel = new ProductModel();
$this->inventoryService = new InventoryService(Database::getConnection());
}
/**
* 显示购物车页面
*/
public function index()
{
$cartItems = $this->cartManager->getCartContents();
$cartData = [];
$totalAmount = 0;
// 补充商品详细信息并验证库存
foreach ($cartItems as $productId => $item) {
$product = $this->productModel->getProductById($productId);
if (!$product) {
// 商品已删除,从购物车移除
$this->cartManager->removeFromCart($productId);
continue;
}
$available = $this->inventoryService->checkStock($productId, $item['quantity']);
$cartData[] = [
'product_id' => $productId,
'name' => $product['name'],
'price' => $product['price'],
'image' => $product['image'],
'quantity' => $item['quantity'],
'subtotal' => $product['price'] * $item['quantity'],
'available' => $available,
'max_quantity' => min($product['stock'], 10) // 限制最大购买数量
];
$totalAmount += $product['price'] * $item['quantity'];
}
// 渲染购物车页面
View::render('cart/index', [
'cartItems' => $cartData,
'totalAmount' => $totalAmount,
'totalItems' => $this->cartManager->getTotalItems()
]);
}
/**
* 结算页面
*/
public function checkout()
{
// 验证用户是否登录
if (!Auth::check()) {
Session::set('redirect_url', '/cart/checkout');
redirect('/login');
}
$cartItems = $this->cartManager->getCartContents();
if (empty($cartItems)) {
Session::set('error', '购物车为空');
redirect('/cart');
}
// 检查库存
$stockCheck = $this->inventoryService->batchCheckStock($cartItems);
$hasStockIssue = false;
foreach ($stockCheck as $productId => $result) {
if (!$result['success']) {
$hasStockIssue = true;
Session::add('errors', $result['message']);
}
}
if ($hasStockIssue) {
redirect('/cart');
}
// 获取用户默认收货地址
$userAddress = AddressModel::getDefaultAddress(Auth::id());
View::render('cart/checkout', [
'cartItems' => $cartItems,
'totalAmount' => $this->cartManager->getTotalAmount(),
'userAddress' => $userAddress
]);
}
/**
* 提交订单
*/
public function placeOrder()
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
redirect('/cart/checkout');
}
try {
// 验证表单数据
$validator = new Validator($_POST);
$validator->validate([
'shipping_name' => 'required|min:2|max:100',
'shipping_phone' => 'required|phone',
'shipping_address' => 'required|min:10',
'payment_method' => 'required|in:alipay,wechat,bank'
]);
if ($validator->fails()) {
Session::set('errors', $validator->errors());
redirect('/cart/checkout');
}
$cartItems = $this->cartManager->getCartContents();
if (empty($cartItems)) {
throw new Exception('购物车为空');
}
// 再次检查库存
$stockCheck = $this->inventoryService->batchCheckStock($cartItems);
foreach ($stockCheck as $result) {
if (!$result['success']) {
throw new Exception($result['message']);
}
}
// 创建订单
$orderService = new OrderService();
$order = $orderService->createOrder(
Auth::id(),
$cartItems,
$_POST['shipping_name'],
$_POST['shipping_phone'],
$_POST['shipping_address'],
$_POST['payment_method']
);
// 清空购物车
$this->cartManager->clearCart();
// 发送订单确认邮件
$mailService = new MailService(Config::get('mail'));
$user = Auth::user();
$mailService->sendOrderConfirmation($order, $user->email);
Session::set('success', '订单创建成功!');
redirect("/orders/{$order->id}");
} catch (Exception $e) {
Session::set('error', $e->getMessage());
redirect('/cart/checkout');
}
}
}
?>
步骤 3:订单服务实现
<?php
class OrderService
{
private $db;
private $inventoryService;
private $mailService;
public function __construct()
{
$this->db = Database::getConnection();
$this->inventoryService = new InventoryService($this->db);
$this->mailService = new MailService(Config::get('mail'));
}
/**
* 创建订单
*/
public function createOrder($userId, $cartItems, $shippingName, $shippingPhone, $shippingAddress, $paymentMethod)
{
$this->db->beginTransaction();
try {
// 生成订单号
$orderNumber = $this->generateOrderNumber();
// 计算订单总金额
$totalAmount = 0;
$orderItems = [];
foreach ($cartItems as $productId => $item) {
$product = (new ProductModel())->getProductById($productId);
if (!$product) {
throw new Exception("商品不存在: {$productId}");
}
$subtotal = $product['price'] * $item['quantity'];
$totalAmount += $subtotal;
$orderItems[] = [
'product_id' => $productId,
'product_name' => $product['name'],
'product_price' => $product['price'],
'quantity' => $item['quantity'],
'subtotal' => $subtotal
];
}
// 创建订单记录
$orderStmt = $this->db->prepare("
INSERT INTO orders
(order_number, user_id, total_amount, shipping_name, shipping_phone, shipping_address, payment_method)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$orderStmt->execute([
$orderNumber, $userId, $totalAmount,
$shippingName, $shippingPhone, $shippingAddress, $paymentMethod
]);
$orderId = $this->db->lastInsertId();
// 创建订单商品记录
$itemStmt = $this->db->prepare("
INSERT INTO order_items
(order_id, product_id, product_name, product_price, quantity, subtotal)
VALUES (?, ?, ?, ?, ?, ?)
");
foreach ($orderItems as $item) {
$itemStmt->execute([
$orderId, $item['product_id'], $item['product_name'],
$item['product_price'], $item['quantity'], $item['subtotal']
]);
}
// 扣减库存
$deductResults = $this->inventoryService->batchDeductStock($cartItems);
foreach ($deductResults as $result) {
if (!$result['success']) {
throw new Exception($result['message']);
}
}
$this->db->commit();
// 返回订单对象
return $this->getOrderById($orderId);
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
/**
* 生成唯一订单号
*/
private function generateOrderNumber()
{
return date('YmdHis') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
}
/**
* 根据ID获取订单
*/
public function getOrderById($orderId)
{
$stmt = $this->db->prepare("
SELECT o.*, u.username, u.email
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.id = ?
");
$stmt->execute([$orderId]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if ($order) {
// 获取订单商品
$itemStmt = $this->db->prepare("
SELECT * FROM order_items WHERE order_id = ?
");
$itemStmt->execute([$orderId]);
$order['items'] = $itemStmt->fetchAll(PDO::FETCH_ASSOC);
// 获取状态历史
$logStmt = $this->db->prepare("
SELECT * FROM order_state_logs
WHERE order_id = ?
ORDER BY created_at ASC
");
$logStmt->execute([$orderId]);
$order['state_logs'] = $logStmt->fetchAll(PDO::FETCH_ASSOC);
}
return $order;
}
}
?>
项目测试指南:
- 功能测试:
- 测试购物车添加、删除、数量修改功能
- 测试库存不足时的提示和处理
- 测试订单创建和状态流转
- 测试邮件通知发送
- 性能测试:
- 模拟高并发下的库存扣减
- 测试购物车 Session 存储性能
- 验证 AJAX 请求的响应时间
- 安全测试:
- 测试订单金额篡改防护
- 验证库存扣减的原子性
- 检查 Session 安全性
部署指南:
- 导入数据库表结构
- 配置邮件服务器参数
- 设置 Session 存储路径和过期时间
- 配置 Redis 用于高并发库存控制(可选)
- 设置定时任务清理过期购物车数据
最佳实践
1. 会话安全最佳实践
安全漏洞案例:Session 固定攻击
// 不安全的Session管理
session_start();
$_SESSION['user_id'] = $user['id'];
// 安全的Session管理
session_start();
session_regenerate_id(true); // 重新生成Session ID
$_SESSION['user_id'] = $user['id'];
$_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR']; // 绑定IP
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT']; // 绑定浏览器
// Session验证
function validateSession()
{
if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR'] ||
$_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
session_destroy();
redirect('/login');
}
}
2. 库存管理最佳实践
超卖问题解决方案:
// 方案1:数据库悲观锁
public function deductStockWithLock($productId, $quantity)
{
$this->db->beginTransaction();
try {
// 锁定商品记录
$stmt = $this->db->prepare("
SELECT stock FROM products
WHERE id = ? FOR UPDATE
");
$stmt->execute([$productId]);
$product = $stmt->fetch();
if ($product['stock'] < $quantity) {
throw new Exception('库存不足');
}
// 扣减库存
$updateStmt = $this->db->prepare("
UPDATE products SET stock = stock - ?
WHERE id = ?
");
$updateStmt->execute([$quantity, $productId]);
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
// 方案2:Redis原子操作
public function deductStockWithRedis($productId, $quantity)
{
$key = "stock:{$productId}";
$lua = "
local stock = tonumber(redis.call('get', KEYS[1]))
local quantity = tonumber(ARGV[1])
if not stock then
return -1 -- 商品不存在
end
if stock < quantity then
return -2 -- 库存不足
end
redis.call('decrby', KEYS[1], quantity)
return stock - quantity
";
$result = $this->redis->eval($lua, [$key, $quantity], 1);
if ($result == -1) {
throw new Exception('商品不存在');
} elseif ($result == -2) {
throw new Exception('库存不足');
}
return $result;
}
3. 订单安全防护
金额篡改防护:
class OrderSecurity
{
/**
* 验证订单金额,防止前端篡改
*/
public static function validateOrderAmount($calculatedAmount, $submittedAmount)
{
// 允许小的浮点数误差
if (abs($calculatedAmount - $submittedAmount) > 0.01) {
throw new Exception('订单金额异常,请重新下单');
}
return true;
}
/**
* 生成订单签名,防止数据篡改
*/
public static function generateOrderSignature($orderData, $secretKey)
{
ksort($orderData);
$signString = http_build_query($orderData) . $secretKey;
return md5($signString);
}
/**
* 验证订单签名
*/
public static function verifyOrderSignature($orderData, $signature, $secretKey)
{
$expectedSignature = self::generateOrderSignature($orderData, $secretKey);
return hash_equals($expectedSignature, $signature);
}
}
// 使用示例
$orderData = [
'order_number' => '20240101000001',
'total_amount' => '199.00',
'user_id' => 123
];
$signature = OrderSecurity::generateOrderSignature($orderData, 'your_secret_key');
// 将签名随订单数据一起存储或传输
// 验证时
if (!OrderSecurity::verifyOrderSignature($orderData, $receivedSignature, 'your_secret_key')) {
throw new Exception('订单数据被篡改');
}
4. 性能优化建议
// 购物车数据缓存优化
class OptimizedCartManager extends CartManager
{
private $cache;
public function __construct()
{
$this->cache = new RedisCache();
}
public function getCartContents()
{
$userId = Auth::id();
if ($userId) {
$cacheKey = "user_cart:{$userId}";
$cached = $this->cache->get($cacheKey);
if ($cached !== false) {
return $cached;
}
}
$contents = parent::getCartContents();
if ($userId) {
$this->cache->set($cacheKey, $contents, 3600); // 缓存1小时
}
return $contents;
}
}
// 批量数据库操作优化
class BatchOrderProcessor
{
public function batchUpdateOrderStatus($orderIds, $newStatus)
{
// 使用IN查询而不是循环单个更新
$placeholders = str_repeat('?,', count($orderIds) - 1) . '?';
$stmt = $this->db->prepare("
UPDATE orders SET status = ?
WHERE id IN ({$placeholders})
");
$params = array_merge([$newStatus], $orderIds);
$stmt->execute($params);
// 批量记录状态变更
$logStmt = $this->db->prepare("
INSERT INTO order_state_logs
(order_id, from_state, to_state, operator)
VALUES (?, ?, ?, ?)
");
foreach ($orderIds as $orderId) {
// 这里可以优化为批量插入
$logStmt->execute([$orderId, 'previous_state', $newStatus, 'batch_operator']);
}
}
}
练习题与挑战
基础练习题
- 购物车 Session 持久化(难度:★☆☆☆)
- 题目:实现购物车数据在用户登录后的持久化功能,将 Session 中的购物车数据保存到数据库,并在用户下次登录时自动加载。
- 要求:
- 设计购物车数据表结构
- 实现 Session 与数据库的购物车数据同步
- 处理未登录用户和已登录用户的购物车合并
- 解题提示:在用户登录成功后,合并 Session 购物车和数据库购物车数据。
- 订单状态查询接口(难度:★☆☆☆)
- 题目:创建一个 RESTful API 接口,支持查询订单详情和状态历史。
- 要求:
- 实现 GET /api/orders/{id} 接口
- 返回订单基本信息和状态变更历史
- 添加适当的权限验证
- 参考答案:参考订单服务类的 getOrderById 方法进行扩展。
进阶练习题
- 库存预警系统(难度:★★☆☆)
- 题目:实现库存预警功能,当商品库存低于设定阈值时自动发送邮件通知管理员。
- 要求:
- 允许管理员为每个商品设置库存预警阈值
- 实现定时检查库存水平
- 发送包含库存详情的预警邮件
- 解题提示:可以使用 Cron 定时任务调用库存检查脚本。
- 购物车商品有效性验证(难度:★★☆☆)
- 题目:在购物车页面显示时,验证每个商品是否仍然有效(如下架、删除、库存变化等)。
- 要求:
- 实时检查商品状态和库存
- 自动移除无效商品并提示用户
- 更新购物车总金额和商品数量
- 参考答案:在购物车控制器的 index 方法中实现商品验证逻辑。
- 订单取消与库存恢复(难度:★★☆☆)
- 题目:实现订单取消功能,取消订单时需要恢复库存并更新订单状态。
- 要求:
- 只有特定状态(待支付、已支付)的订单可以取消
- 取消订单时恢复对应商品的库存
- 记录取消原因和操作人
- 解题提示:使用订单状态机验证状态转换的合法性。
综合挑战题
- 高并发库存扣减系统(难度:★★★☆)
- 题目:设计一个支持高并发场景的库存扣减系统,防止超卖问题。
- 要求:
- 使用 Redis 实现库存缓存和原子操作
- 实现库存预扣机制
- 设计库存回滚机制
- 解题提示:结合数据库和 Redis,使用 Lua 脚本保证原子性。
- 分布式购物车系统(难度:★★★★)
- 题目:设计支持多服务器部署的分布式购物车系统,解决 Session 共享问题。
- 要求:
- 使用 Redis 集中存储购物车数据
- 实现购物车数据的序列化和反序列化
- 保证数据一致性和性能
- 解题提示:重写购物车管理类,使用 Redis 代替 Session 存储。
章节总结
本章重点知识回顾:
- 购物车系统设计:掌握了使用 Session 存储购物车数据的原理和实现方法,了解了购物车数据的持久化策略。
- AJAX 异步操作:学会了使用 AJAX 技术实现购物车的实时更新,提升了用户体验。
- 订单状态机:理解了订单状态机的设计理念,实现了订单状态的规范流转。
- 库存管理:掌握了防止超卖的多种技术方案,包括数据库锁和 Redis 原子操作。
- 邮件通知:集成了邮件通知系统,实现了订单状态变化的自动通知。
技能掌握要求:
- 能够独立设计并实现完整的购物车功能
- 能够处理订单的完整生命周期管理
- 能够实现可靠的库存控制系统
- 能够集成第三方服务(如邮件服务)
- 理解电商系统中的常见安全问题并实施防护
与下一章的衔接预告:
在第 4 章中,我们将深入探讨支付网关集成与网站安全防护。你将学习如何集成支付宝和微信支付接口,实施 SSL 加密传输,防范 SQL 注入和 XSS 攻击,确保电商平台的交易安全和数据保护。
进一步学习建议:
- 消息队列:学习使用 Redis 或 RabbitMQ 实现邮件发送的异步处理
- 缓存策略:深入研究 Redis 在电商系统中的应用,如购物车缓存、库存缓存等
- 微服务架构:了解如何将购物车、订单、库存等服务拆分为独立的微服务
- 性能监控:学习使用 APM 工具监控电商系统的性能瓶颈
通过本章的学习,你已经掌握了电商核心的购物车和订单处理功能,为构建完整的电商平台奠定了坚实基础。这些技能不仅适用于教学项目,在实际的商业项目开发中同样具有重要价值。
更多推荐

所有评论(0)