☰
PHP化妆品销售网站课程设计:SPU/SKU建表与下单链路实现
2026/10/10 1:57:16 网站建设 项目流程

简介:这份资源是一份基于PHP的化妆品销售网站毕业设计初稿文档,面向计算机相关专业学生及需要完成课程设计或毕业设计的学习者,帮助解决在线购物系统从需求分析到功能落地的完整实现问题。压缩包内仅含1个docx文件,约475KB,内容为完整的论文初稿,涵盖摘要、引言、系统架构与开发工具、系统分析、设计思路、详细实现、系统测试与总结等章节。文档以B/S架构为基础,采用PHP搭建网站,配合Sublime Text开发工具与Navicat for MySQL数据库,详细阐述了会员注册登录、购物车管理、产品浏览收藏、订单处理、商品管理、用户信息管理及订单管理等模块的设计与实现过程。读者可从中获取完整的系统分析思路、数据库设计方案、前后台功能模块实现方法以及测试用例与结果分析,适合作为PHP电商类项目开发的参考案例,也可用于学习如何撰写规范的毕业设计论文。目前已有39人学习,具备一定的参考价值。

1. 从一份课程设计文档到能跑起来的化妆品销售站:先想清楚边界

很多同学拿到“基于PHP的化妆品销售网站的设计与实现”这个题目,第一反应是打开编辑器写商品列表,写到一半发现购物车、订单、库存三块逻辑互相打架,最后交上去的是一堆能点但不敢下单的页面。我在帮人看这类课程设计时,最常见的翻车点不是PHP语法,而是把“销售网站”当成“商品展示页”来做。化妆品这个品类还有它的特殊性:同一款粉底液有多个色号,同一色号可能分装和正装两个SKU,库存要按SKU扣,价格要按活动价算,这些如果不在建表阶段想清楚,后面改起来就是血泪经验。这篇文章面向的是正在做PHP课程设计、需要一套能演示下单流程的销售站的同学,也适合想用PHP把电商核心链路跑通一遍的初级开发者。我会按“需求边界→数据库→核心链路→后台→避坑→进阶”的顺序,把每一步的参数和判断依据讲清楚,代码可以直接抄,但表结构建议你按自己的SKU粒度再调一次。

2. 需求边界与建表:化妆品销售站和普通商城差在哪

2.1 先砍掉不需要的功能,再谈实现

课程设计最容易失控的地方是功能列表越写越长。我的建议是先把功能分成“演示必须”和“可以不做”两档。演示必须的是:商品分类浏览、商品详情(含SKU选择)、加入购物车、下单生成订单、订单列表、后台商品管理。可以不做的是:优惠券叠加、积分体系、多级分销、真实支付对接。支付环节用模拟支付回调即可,重点是订单状态机要能走通“待支付→已支付→已发货→已完成”。

化妆品销售站和普通商城的差异集中在商品模型上。普通商城一个商品一个价格一个库存,化妆品往往是一个SPU(比如“某品牌持妆粉底液”)下面挂多个SKU(色号+规格),每个SKU独立库存和价格。如果你按普通商城建表,后面加色号就要改表结构,这是典型的后悔药场景。所以建表阶段就要把SPU和SKU拆开。

2.2 核心表结构与字段说明

下面这套表结构是我在多个课程设计里验证过的,字段够用且不臃肿。注意所有表名和字段名都用小写加下划线,避免在不同操作系统上出现大小写问题。

-- 商品SPU表:只存公共信息 CREATE TABLE `product` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `name` varchar(120) NOT NULL COMMENT '商品名称', `category_id` int unsigned NOT NULL COMMENT '分类ID', `brand` varchar(60) DEFAULT '' COMMENT '品牌', `main_image` varchar(255) DEFAULT '' COMMENT '主图路径', `description` text COMMENT '详情描述', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- SKU表:色号、规格、独立库存和价格 CREATE TABLE `product_sku` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `product_id` int unsigned NOT NULL, `sku_name` varchar(80) NOT NULL COMMENT '如 象牙白/30ml', `price` decimal(10,2) NOT NULL DEFAULT 0.00, `stock` int NOT NULL DEFAULT 0, `sku_code` varchar(60) DEFAULT '' COMMENT 'SKU编码', PRIMARY KEY (`id`), KEY `idx_product` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:金额用decimal,状态用tinyint CREATE TABLE `order` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` int unsigned NOT NULL, `total_amount` decimal(10,2) NOT NULL DEFAULT 0.00, `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成 4已取消', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细:下单时把SKU名称和价格快照进来 CREATE TABLE `order_item` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `order_id` int unsigned NOT NULL, `sku_id` int unsigned NOT NULL, `sku_name` varchar(80) NOT NULL COMMENT '下单时快照', `price` decimal(10,2) NOT NULL COMMENT '下单时快照', `quantity` int NOT NULL DEFAULT 1, PRIMARY KEY (`id`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有两个参数值得单独说。第一,金额字段统一用decimal(10,2),不要用float,浮点累加会出现0.30000000000000004这种结果,对账时非常难受。第二,order_item里的sku_name和price是快照字段,下单那一刻把名称和价格复制进来,之后后台改价不影响历史订单。很多同学省掉这两个字段,结果后台调价后历史订单金额跟着变,这是排查起来很隐蔽的坑。

2.3 分类表与用户表的简化处理

分类表支持两级即可,用parent_id自关联。化妆品常见的一级分类是“护肤、彩妆、香氛、工具”,二级分类挂在下面。用户表课程设计里不需要太复杂,id、username、password_hash、created_at四个字段就够,密码用password_hash()生成,登录用password_verify()校验,不要存明文,这是最基本的安全习惯。

建完表之后,建议先手动插入两条SPU和四条SKU数据,把查询跑通再写页面。这一步花十分钟,能省掉后面调试时怀疑“到底是SQL错了还是PHP错了”的时间。

3. 用PHP跑通下单主链路:从加购到订单落库

3.1 数据库连接与公共配置

先建一个config.php,把连接参数和公共函数放进去。课程设计里不需要上框架,原生PDO足够,而且能让你看清每一步在做什么。

<?php // config.php $dbHost = '127.0.0.1'; $dbName = 'cosmetic_shop'; $dbUser = 'root'; $dbPass = 'your_password'; $dsn = "mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4"; try { $pdo = new PDO($dsn, $dbUser, $dbPass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 出错抛异常,别用静默模式 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, // 用真正的预处理,防注入 ]); } catch (PDOException $e) { exit('数据库连接失败:' . $e->getMessage()); } session_start(); // 生成订单号:时间戳+随机数,够课程设计用 function makeOrderNo(): string { return date('YmdHis') . str_pad((string)random_int(0, 9999), 4, '0', STR_PAD_LEFT); }

ATTR_EMULATE_PREPARES => false这个参数建议打开,它让PDO使用MySQL原生的预处理语句,而不是在PHP层拼接。配合占位符使用,能挡住绝大多数SQL注入。ERRMODE_EXCEPTION也要开,否则SQL出错时PDO默认静默返回false,你会对着空白页面发呆。

3.2 加入购物车:用session存还是用表存

课程设计里购物车有两种做法:存session和存数据库表。存session实现快,但换浏览器就丢;存表更接近真实系统,但要处理登录态。我的建议是登录用户存表,未登录存session,登录时合并。为了控制篇幅,这里演示存表的做法,表结构加一个cart表:id、user_id、sku_id、quantity,对user_id+sku_id建唯一索引。

<?php // cart_add.php 加入购物车 require 'config.php'; $userId = $_SESSION['user_id'] ?? 0; $skuId = (int)($_POST['sku_id'] ?? 0); $qty = max(1, (int)($_POST['quantity'] ?? 1)); if ($userId <= 0) { exit(json_encode(['code' => 401, 'msg' => '请先登录'])); } // 检查SKU是否存在且有库存 $stmt = $pdo->prepare('SELECT id, stock FROM product_sku WHERE id = ?'); $stmt->execute([$skuId]); $sku = $stmt->fetch(); if (!$sku) { exit(json_encode(['code' => 404, 'msg' => '商品不存在'])); } if ($sku['stock'] < $qty) { exit(json_encode(['code' => 400, 'msg' => '库存不足'])); } // 存在则累加,不存在则插入 $stmt = $pdo->prepare('INSERT INTO cart (user_id, sku_id, quantity) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)'); $stmt->execute([$userId, $skuId, $qty]); echo json_encode(['code' => 0, 'msg' => '已加入购物车']);

这里用ON DUPLICATE KEY UPDATE依赖user_id+sku_id的唯一索引,一条SQL完成“有则累加、无则插入”。注意quantity + VALUES(quantity)里的VALUES()在MySQL 8.0.20之后被标记为过时,新版本可以用别名写法,但课程设计环境通常是5.7或8.0早期版本,这样写兼容性最好。

3.3 下单:事务、库存扣减与订单快照

下单是整个链路里最不能省事务的地方。扣库存、写订单、写明细、清购物车,四步要么全成功要么全回滚。下面这段代码是核心,建议逐行看注释。

<?php // order_create.php 创建订单 require 'config.php'; $userId = $_SESSION['user_id'] ?? 0; if ($userId <= 0) { exit(json_encode(['code' => 401, 'msg' => '请先登录'])); } // 取出购物车中选中的SKU $stmt = $pdo->prepare('SELECT c.sku_id, c.quantity, s.price, s.stock, s.sku_name FROM cart c JOIN product_sku s ON c.sku_id = s.id WHERE c.user_id = ?'); $stmt->execute([$userId]); $items = $stmt->fetchAll(); if (!$items) { exit(json_encode(['code' => 400, 'msg' => '购物车为空'])); } $pdo->beginTransaction(); try { $total = 0; foreach ($items as $it) { // 扣库存时带stock >= quantity条件,防止超卖 $upd = $pdo->prepare('UPDATE product_sku SET stock = stock - ? WHERE id = ? AND stock >= ?'); $upd->execute([$it['quantity'], $it['sku_id'], $it['quantity']]); if ($upd->rowCount() === 0) { throw new Exception('库存不足:' . $it['sku_name']); } $total += $it['price'] * $it['quantity']; } // 写订单主表 $orderNo = makeOrderNo(); $ins = $pdo->prepare('INSERT INTO `order` (order_no, user_id, total_amount, status) VALUES (?, ?, ?, 0)'); $ins->execute([$orderNo, $userId, $total]); $orderId = $pdo->lastInsertId(); // 写订单明细,价格和名称做快照 $insItem = $pdo->prepare('INSERT INTO order_item (order_id, sku_id, sku_name, price, quantity) VALUES (?, ?, ?, ?, ?)'); foreach ($items as $it) { $insItem->execute([$orderId, $it['sku_id'], $it['sku_name'], $it['price'], $it['quantity']]); } // 清空该用户购物车 $pdo->prepare('DELETE FROM cart WHERE user_id = ?')->execute([$userId]); $pdo->commit(); echo json_encode(['code' => 0, 'order_no' => $orderNo]); } catch (Exception $e) { $pdo->rollBack(); echo json_encode(['code' => 500, 'msg' => $e->getMessage()]); }

关键点在扣库存的SQL:WHERE id = ? AND stock >= ?,把库存判断和扣减放在同一条语句里,利用数据库行锁保证并发下不会扣成负数。如果你先查库存再扣,两个请求同时查到库存为1,就会超卖。rowCount() === 0表示这条更新没命中,说明库存不够,直接抛异常回滚。订单明细里的sku_name和price来自下单时的查询结果,写进去就是快照,后续后台改价不影响它。

3.4 模拟支付回调与订单状态流转

课程设计不需要接真实支付,写一个模拟回调接口即可。用户点“去支付”后,前端请求这个接口,把订单状态从0改成1。

<?php // pay_notify.php 模拟支付成功 require 'config.php'; $orderNo = $_POST['order_no'] ?? ''; $userId = $_SESSION['user_id'] ?? 0; // 只允许把待支付订单改成已支付,防止重复回调 $stmt = $pdo->prepare('UPDATE `order` SET status = 1 WHERE order_no = ? AND user_id = ? AND status = 0'); $stmt->execute([$orderNo, $userId]); if ($stmt->rowCount() > 0) { echo json_encode(['code' => 0, 'msg' => '支付成功']); } else { echo json_encode(['code' => 400, 'msg' => '订单状态异常或已支付']); }

status = 0这个条件很重要,它保证同一个订单不会被重复支付。真实支付回调里还会验签、校验金额,课程设计里把状态机守住就够了。订单状态建议固定为:0待支付、1已支付、2已发货、3已完成、4已取消,后台发货时只允许从1改到2,不要允许跳状态。

4. 后台商品管理与SKU编辑:别让运营改坏数据

4.1 商品列表与SKU的联动编辑

后台最容易被忽略的是SKU编辑。一个SPU下面有多个SKU,运营改价格时如果只改SPU,前端展示的价格就对不上。我的做法是后台商品列表只展示SPU,点进去再展示SKU列表,每个SKU独立编辑价格和库存。这样职责清晰,也不容易误操作。

<?php // admin/product_edit.php 保存SKU修改 require '../config.php'; // 假设已做后台登录校验 $productId = (int)($_POST['product_id'] ?? 0); $skus = $_POST['skus'] ?? []; // 格式:skus[sku_id][price], skus[sku_id][stock] $pdo->beginTransaction(); try { $upd = $pdo->prepare('UPDATE product_sku SET price = ?, stock = ? WHERE id = ? AND product_id = ?'); foreach ($skus as $skuId => $row) { $price = number_format((float)$row['price'], 2, '.', ''); $stock = max(0, (int)$row['stock']); $upd->execute([$price, $stock, (int)$skuId, $productId]); } $pdo->commit(); echo json_encode(['code' => 0]); } catch (Exception $e) { $pdo->rollBack(); echo json_encode(['code' => 500, 'msg' => '保存失败']); }

WHERE id = ? AND product_id = ?这个双条件是为了防止有人改URL里的SKU ID去改别的商品的SKU。后台接口一定要校验数据归属,不能只靠前端传参。价格用number_format格式化到两位小数再入库,避免用户输入19.999这种值。

4.2 图片上传的路径与格式校验

化妆品商品图多,上传功能要限制格式和大小。建议只允许jpg、jpeg、png、webp,大小限制2MB以内,文件名用uniqid()重命名,不要用用户上传的原始文件名,防止路径穿越。

<?php // admin/upload.php $allow = ['jpg', 'jpeg', 'png', 'webp']; $maxSize = 2 * 1024 * 1024; if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) { exit(json_encode(['code' => 400, 'msg' => '上传失败'])); } if ($_FILES['image']['size'] > $maxSize) { exit(json_encode(['code' => 400, 'msg' => '图片不能超过2MB'])); } $ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION)); if (!in_array($ext, $allow, true)) { exit(json_encode(['code' => 400, 'msg' => '只支持jpg/png/webp'])); } $dir = __DIR__ . '/../uploads/' . date('Ym'); if (!is_dir($dir)) { mkdir($dir, 0755, true); } $filename = uniqid('img_', true) . '.' . $ext; move_uploaded_file($_FILES['image']['tmp_name'], $dir . '/' . $filename); echo json_encode(['code' => 0, 'path' => '/uploads/' . date('Ym') . '/' . $filename]);

按月份分目录是为了避免单目录文件过多。uniqid('img_', true)加true参数会生成带熵的字符串,降低重名概率。上传目录不要给执行权限,配置里加一条禁止PHP执行,这是基本的安全习惯。

5. 避坑与排查:课程设计里最容易翻车的五件事

5.1 中文乱码:从连接、表到页面三层都要查

现象是商品名显示成问号或乱码。原因通常是三层里有一层没设utf8mb4:数据库连接、表字符集、页面输出。解决方法是连接DSN里写charset=utf8mb4,建表时指定DEFAULT CHARSET=utf8mb4,PHP输出HTML时加header('Content-Type: text/html; charset=utf-8')。三层都对齐就不会乱。注意utf8mb4和utf8在MySQL里不是一回事,utf8mb4才能存完整的四字节字符。

5.2 库存扣成负数:并发下先查后扣的经典错误

现象是两个人同时下单,库存1件却卖出2件。原因是先SELECT stock再UPDATE stock = stock - 1,两个请求都查到1,都执行扣减。解决方法是用UPDATE ... WHERE stock >= ?,把判断和扣减合并,靠行锁保证原子性,并检查rowCount()。如果rowCount()为0就回滚。这个坑在课程设计答辩时经常被问到,建议提前想清楚怎么解释。

5.3 订单金额对不上:浮点运算和快照缺失

现象是订单总额和明细相加差几分钱。原因有两个:一是用了float累加,二是明细里没存下单时的价格,后台改价后重新计算就变了。解决方法是用decimal存金额,PHP里用number_format或bcadd处理,明细表存价格快照。对账时以订单主表的total_amount为准,明细只做展示。

5.4 重复提交订单:刷新页面就多一单

现象是用户点下单后刷新页面,又生成一单。原因是下单接口没有防重。解决方法有两个层面:前端点一次后禁用按钮,后端在订单表加唯一约束或在下单前检查最近几秒内是否有同用户同金额的待支付订单。更稳妥的是引入表单令牌,每次下单页生成一个token,提交时校验并销毁。

5.5 后台越权:改URL就能看别人的订单

现象是把订单详情URL里的订单ID改一下,就能看到别人的订单。原因是查询时只用了WHERE id = ?,没加user_id条件。解决方法是所有涉及用户数据的查询都带上user_id = ?,后台接口则校验管理员身份。这是最容易被忽略但后果最严重的问题,答辩时如果被问到会很难解释。

6. 进阶:把订单状态机和库存流水补上

课程设计做到上面那一步已经能演示完整流程,但如果你想让它更接近真实系统,有两个方向值得投入。第一个是订单状态机。现在状态流转散落在各个接口里,容易改乱。可以把允许的流转写成一张配置表,比如0→1、1→2、2→3、0→4、1→4,每次改状态前查一下是否允许,不允许就拒绝。这样后台加一个“取消订单”功能时,不会误把已发货订单取消掉。

第二个是库存流水。现在库存只有product_sku.stock一个数字,扣了就没了,查不到为什么少。可以加一张stock_log表,记录sku_id、change_type(下单扣减/取消回补/后台调整)、quantity、before_stock、after_stock、created_at。每次扣库存时在同一个事务里写一条流水。这样对账时能追溯每一笔变动,后台也能看到库存变化历史。

CREATE TABLE `stock_log` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `sku_id` int unsigned NOT NULL, `change_type` varchar(20) NOT NULL COMMENT 'order_deduct/order_cancel/admin_edit', `quantity` int NOT NULL COMMENT '正数增加负数减少', `before_stock` int NOT NULL, `after_stock` int NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_sku` (`sku_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

写流水时注意before_stock和after_stock要在同一条SQL里算出来,或者先查再写但放在事务里。我一般会在扣减SQL执行后,用SELECT stock FROM product_sku WHERE id = ?再查一次拿到after_stock,before_stock就是after_stock + quantity。这样虽然多一次查询,但逻辑清晰,不容易算错。

验证方法很简单:下三单,取消一单,后台手动改一次库存,然后查stock_log,看流水加减是否和product_sku.stock的最终值一致。如果不一致,说明某条路径漏写了流水。这个习惯我从第一次做电商项目就保留到现在,库存对不上时,流水表就是唯一的黑匣子。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询