PHP化妆品销售网站课程设计:SPU/SKU建表与下单链路实现
简介这份资源是一份基于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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表金额用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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细下单时把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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个参数值得单独说。第一金额字段统一用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};charsetutf8mb4; 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_idsku_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_idsku_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()]); }关键点在扣库存的SQLWHERE 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里写charsetutf8mb4建表时指定DEFAULT CHARSETutf8mb4PHP输出HTML时加header(Content-Type: text/html; charsetutf-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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;写流水时注意before_stock和after_stock要在同一条SQL里算出来或者先查再写但放在事务里。我一般会在扣减SQL执行后用SELECT stock FROM product_sku WHERE id ?再查一次拿到after_stockbefore_stock就是after_stock quantity。这样虽然多一次查询但逻辑清晰不容易算错。验证方法很简单下三单取消一单后台手动改一次库存然后查stock_log看流水加减是否和product_sku.stock的最终值一致。如果不一致说明某条路径漏写了流水。这个习惯我从第一次做电商项目就保留到现在库存对不上时流水表就是唯一的黑匣子。希望帮到你。本文还有配套的精品资源点击获取