简介:本资源为基于PHP+MySQL实现的服装商城购物系统课程设计项目,编号100013121,面向计算机相关专业学生及Web开发初学者,可用于课程设计参考、PHP与数据库综合实践或毕业设计选题。系统涵盖用户管理、商品展示、购物车与后台订单管理等模块,涉及表单处理、数据库设计、SQL查询优化、事务处理及SQL注入防护等知识点,并采用响应式布局兼顾多端体验。压缩包共260个文件,约5.54MB,包含10个PHP核心页面、1个SQL数据库脚本、9个HTML与9个CSS样式文件、21个JavaScript脚本,以及大量jpg、png、gif图片素材和字体文件,另附项目说明书文档,便于理解整体架构与运行流程。目前已有393人学习下载,适合需要完整项目源码、数据库脚本与说明文档的读者参考复用。
1. 服装商城购物系统拆包:一份能跑起来的 PHP+MySQL 课程设计长什么样
很多同学做课程设计时,最头疼的不是写代码,而是找不到一个结构完整、能跑通、还带数据库的参考项目。这份编号 100013121 的服装商城购物系统,就是典型的 PHP+MySQL 组合:前端用原生 PHP 渲染页面,后端用 MySQL 存商品、订单、用户数据,功能覆盖商品浏览、购物车、下单、后台管理这条主线。它适合正在做 Web 课程设计、想快速理解电商系统数据流的学生,也适合刚接触 PHP 全栈、需要一个完整 CRUD 案例练手的开发者。拿到源码后,你要做的不是通读每一行,而是先把它跑起来,再顺着「用户下单」这条链路去拆表结构和业务逻辑。
2. 环境搭建与数据库初始化:从 MySQL 安装到导入 SQL 文件
2.1 选型理由:为什么这套系统用原生 PHP 而不是框架
这套系统没有用 Laravel、ThinkPHP 这类框架,而是原生 PHP 加 MySQL。原因很直接:课程设计场景下,框架会引入 Composer 依赖、路由配置、模板引擎等额外概念,反而增加理解成本。原生 PHP 的请求流程是「浏览器访问 .php 文件 → PHP 解析执行 → 查询 MySQL → 返回 HTML」,每一步都看得见,适合用来理解 Web 请求的完整生命周期。
数据库层面,MySQL 是这类项目最稳妥的选择。它安装简单、社区资料多、和 PHP 的 mysqli/PDO 扩展配合成熟。你不需要考虑分布式或高并发,单机 MySQL 足够支撑课程设计的演示和答辩。
常见做法是:本地用 phpStudy 或 XAMPP 一键集成环境,省去单独配置 PHP 和 MySQL 的麻烦。但如果你想真正搞懂环境依赖,我建议手动装一遍,踩过的坑才是自己的。
2.2 手动搭建 PHP+MySQL 运行环境
先确认版本匹配。PHP 7.4 或 8.0 都可以,MySQL 用 5.7 或 8.0。PHP 8.0 配 MySQL 8.0 时,mysqli 默认认证插件是 caching_sha2_password,老代码可能连不上,后面避坑章节会细说。
Windows 下安装 MySQL 8 的典型步骤:
# 1. 下载 MySQL 8 的 ZIP 包,解压到 D:\mysql8 # 2. 初始化数据目录 mysqld --initialize-insecure --basedir=D:\mysql8 --datadir=D:\mysql8\data # 3. 注册为系统服务 mysqld --install MySQL8 --defaults-file=D:\mysql8\my.ini # 4. 启动服务 net start MySQL8 # 5. 登录并设置 root 密码 mysql -u root --skip-password ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密码';这段命令的逻辑是:--initialize-insecure创建一个空密码的 root 账户,方便首次登录;--install把 MySQL 注册成 Windows 服务,开机自启;最后用ALTER USER设置正式密码。参数--basedir和--datadir必须指向你实际解压的路径,写错会导致服务启动失败。
Linux 下更简单,Ubuntu 用apt install mysql-server,CentOS 用yum install mysql-server。安装完执行mysql_secure_installation做安全初始化。
PHP 这边,确认php.ini里这两行没有被注释:
extension=mysqli extension=pdo_mysql改完重启 Apache 或 Nginx。写一个info.php内容为<?php phpinfo(); ?>,浏览器访问能看到 mysqli 模块就说明环境通了。
2.3 导入数据库并配置连接参数
源码包里一般有一个.sql文件,比如shop.sql或database.sql。导入方式有两种:
# 方式一:命令行导入 mysql -u root -p 你的密码 < shop.sql # 方式二:先建库再导入 mysql -u root -p CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4; USE shop; SOURCE D:/project/shop.sql;推荐用第二种,因为可以显式指定字符集为utf8mb4,避免中文商品名变成乱码。导入完成后用SHOW TABLES;确认表都建好了,通常会有user、goods、orders、cart、admin这几张核心表。
接下来找到 PHP 的连接配置文件,通常叫conn.php、db.php或config.php,改成你自己的数据库信息:
<?php $host = '127.0.0.1'; // 数据库地址,本地用 127.0.0.1 比 localhost 更稳 $user = 'root'; // 数据库用户名 $pass = '你的密码'; // 数据库密码 $db = 'shop'; // 数据库名,和导入时建库名一致 $port = 3306; // MySQL 默认端口 $conn = new mysqli($host, $user, $pass, $db, $port); if ($conn->connect_error) { die('连接失败: ' . $conn->connect_error); } $conn->set_charset('utf8mb4'); // 必须设置,否则中文乱码 ?>这里用127.0.0.1而不是localhost是有原因的:Linux 下localhost会走 Unix socket 而不是 TCP,如果 MySQL 的 socket 路径和 PHP 默认不一致,就会报error 2002 (HY000): can't connect to local MySQL server through socket。用127.0.0.1强制走 TCP,能绕开这个玄学问题。set_charset('utf8mb4')这行不能省,它决定了 PHP 和 MySQL 之间传输中文时用什么编码。
3. 核心功能链路拆解:商品、购物车、订单三张表的联动
3.1 商品列表与详情页的数据查询逻辑
商品列表页通常对应index.php或goods_list.php。核心逻辑是从goods表查出所有上架商品,分页展示。典型查询:
<?php $page = isset($_GET['page']) ? intval($_GET['page']) : 1; $page = max(1, $page); // 防止页码为 0 或负数 $size = 12; // 每页显示 12 件商品 $offset = ($page - 1) * $size; $sql = "SELECT id, name, price, img, stock FROM goods WHERE status = 1 ORDER BY id DESC LIMIT ?, ?"; $stmt = $conn->prepare($sql); $stmt->bind_param('ii', $offset, $size); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { // 渲染商品卡片 } ?>这段代码的关键点有三个。第一,intval($_GET['page'])把用户输入的页码强制转成整数,防止 SQL 注入。第二,用prepare+bind_param而不是直接拼接 SQL,这是防注入的标准做法。第三,LIMIT ?, ?的两个参数都是整数,所以bind_param的类型字符串是'ii',写错类型会导致查询结果异常。
商品详情页对应goods_detail.php,通过 URL 参数id查询单条记录:
<?php $id = intval($_GET['id']); $sql = "SELECT * FROM goods WHERE id = ? AND status = 1"; $stmt = $conn->prepare($sql); $stmt->bind_param('i', $id); $stmt->execute(); $goods = $stmt->get_result()->fetch_assoc(); if (!$goods) { die('商品不存在或已下架'); } ?>这里加了status = 1的条件,确保已下架商品即使知道 ID 也访问不到。fetch_assoc()返回关联数组,字段名就是表里的列名,模板里直接用$goods['name']、$goods['price']取值。
3.2 购物车:Session 存储与数据库持久化的取舍
购物车是这套系统里最能体现设计思路的模块。常见有两种实现:Session 存储和数据库存储。
Session 方案把购物车数据存在$_SESSION['cart']里,结构类似[goods_id => quantity]。优点是实现简单、不写数据库;缺点是换浏览器或清 Cookie 后购物车丢失,且无法做「购物车商品降价提醒」这类功能。
数据库方案建一张cart表,字段包括id、user_id、goods_id、num、add_time。优点是持久化、可跨设备;缺点是多一次数据库查询。
这套课程设计项目通常用 Session 方案,因为不需要登录就能加购,降低演示门槛。核心代码:
<?php session_start(); // 加入购物车 function addToCart($goodsId, $num = 1) { $goodsId = intval($goodsId); $num = max(1, intval($num)); if (!isset($_SESSION['cart'])) { $_SESSION['cart'] = []; } if (isset($_SESSION['cart'][$goodsId])) { $_SESSION['cart'][$goodsId] += $num; // 已存在则累加数量 } else { $_SESSION['cart'][$goodsId] = $num; // 不存在则新增 } } // 从购物车读取商品详情 function getCartItems($conn) { if (empty($_SESSION['cart'])) { return []; } $ids = array_keys($_SESSION['cart']); $idStr = implode(',', array_map('intval', $ids)); // 强制转整数再拼接 $sql = "SELECT id, name, price, img FROM goods WHERE id IN ($idStr)"; $result = $conn->query($sql); $items = []; while ($row = $result->fetch_assoc()) { $row['num'] = $_SESSION['cart'][$row['id']]; $row['subtotal'] = $row['price'] * $row['num']; $items[] = $row; } return $items; } ?>array_map('intval', $ids)这一步是安全关键。虽然$ids来自 Session 理论上可控,但养成「拼 SQL 前先强制转整数」的习惯能避免很多意外。subtotal是小计,模板里直接用来显示每行商品的总价。
购物车数量修改和删除也是操作$_SESSION['cart']数组,改完重新计算总价即可。注意每次操作后要header('Location: cart.php')跳转回购物车页,避免刷新时重复提交。
3.3 下单流程:从购物车到订单表的事务处理
下单是整个系统里唯一需要事务的地方。因为要同时做三件事:写orders表、写order_items表、扣减goods表库存。任何一步失败都会导致数据不一致。
<?php session_start(); $conn->begin_transaction(); try { $userId = intval($_SESSION['user_id']); $total = 0; $items = getCartItems($conn); if (empty($items)) { throw new Exception('购物车为空'); } // 1. 计算总价并检查库存 foreach ($items as $item) { $stockSql = "SELECT stock FROM goods WHERE id = ? FOR UPDATE"; $stmt = $conn->prepare($stockSql); $stmt->bind_param('i', $item['id']); $stmt->execute(); $stock = $stmt->get_result()->fetch_assoc()['stock']; if ($stock < $item['num']) { throw new Exception('商品 ' . $item['name'] . ' 库存不足'); } $total += $item['subtotal']; } // 2. 写入订单主表 $orderNo = date('YmdHis') . rand(1000, 9999); $stmt = $conn->prepare("INSERT INTO orders (order_no, user_id, total, status, add_time) VALUES (?, ?, ?, 0, NOW())"); $stmt->bind_param('sid', $orderNo, $userId, $total); $stmt->execute(); $orderId = $conn->insert_id; // 3. 写入订单明细并扣库存 foreach ($items as $item) { $stmt = $conn->prepare("INSERT INTO order_items (order_id, goods_id, price, num) VALUES (?, ?, ?, ?)"); $stmt->bind_param('iidi', $orderId, $item['id'], $item['price'], $item['num']); $stmt->execute(); $stmt = $conn->prepare("UPDATE goods SET stock = stock - ? WHERE id = ?"); $stmt->bind_param('ii', $item['num'], $item['id']); $stmt->execute(); } $conn->commit(); unset($_SESSION['cart']); // 清空购物车 header('Location: order_success.php?order_no=' . $orderNo); } catch (Exception $e) { $conn->rollback(); die('下单失败:' . $e->getMessage()); } ?>begin_transaction()开启事务,commit()提交,rollback()回滚。SELECT ... FOR UPDATE是行级锁,防止两个用户同时买最后一件商品时超卖。insert_id拿到刚插入的订单主键,用于关联订单明细。orderNo用时间戳加随机数生成,保证唯一性。整个流程要么全成功,要么全回滚,不会出现「订单写了但库存没扣」的情况。
4. 后台管理与权限控制:管理员登录与商品 CRUD
4.1 管理员登录与 Session 鉴权
后台入口通常是/admin/login.php。管理员表admin存用户名和密码哈希。登录逻辑:
<?php session_start(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim($_POST['username']); $password = $_POST['password']; $stmt = $conn->prepare("SELECT id, username, password FROM admin WHERE username = ?"); $stmt->bind_param('s', $username); $stmt->execute(); $admin = $stmt->get_result()->fetch_assoc(); if ($admin && password_verify($password, $admin['password'])) { $_SESSION['admin_id'] = $admin['id']; $_SESSION['admin_name'] = $admin['username']; header('Location: index.php'); exit; } else { $error = '用户名或密码错误'; } } ?>password_verify是 PHP 内置的密码校验函数,配合password_hash使用。如果源码里存的是 MD5 密码,建议改成password_hash,因为 MD5 已经不安全了。登录成功后把admin_id写进 Session,后续每个后台页面开头都要检查:
<?php session_start(); if (!isset($_SESSION['admin_id'])) { header('Location: login.php'); exit; } ?>这段代码要放在后台每个 PHP 文件的最顶部,session_start()之前不能有任何输出,否则会报headers already sent错误。
4.2 商品管理:新增、编辑、删除与图片上传
商品 CRUD 是后台的核心。新增商品涉及表单提交和图片上传:
<?php // 处理图片上传 if (isset($_FILES['img']) && $_FILES['img']['error'] === UPLOAD_ERR_OK) { $allowTypes = ['image/jpeg', 'image/png', 'image/gif']; $maxSize = 2 * 1024 * 1024; // 2MB if (!in_array($_FILES['img']['type'], $allowTypes)) { die('只允许上传 JPG/PNG/GIF 图片'); } if ($_FILES['img']['size'] > $maxSize) { die('图片不能超过 2MB'); } $ext = pathinfo($_FILES['img']['name'], PATHINFO_EXTENSION); $newName = uniqid('goods_', true) . '.' . $ext; $uploadDir = __DIR__ . '/../uploads/'; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } if (move_uploaded_file($_FILES['img']['tmp_name'], $uploadDir . $newName)) { $imgPath = 'uploads/' . $newName; } else { die('图片上传失败'); } } // 写入数据库 $stmt = $conn->prepare("INSERT INTO goods (name, price, stock, img, status, add_time) VALUES (?, ?, ?, ?, 1, NOW())"); $stmt->bind_param('sdis', $_POST['name'], $_POST['price'], $_POST['stock'], $imgPath); $stmt->execute(); ?>$_FILES['img']['type']是浏览器传来的 MIME 类型,可以被伪造,更严格的做法是用finfo_file()检测真实类型。uniqid('goods_', true)生成唯一文件名,避免同名覆盖。move_uploaded_file把临时文件移到目标目录,目标目录必须存在且有写权限。
编辑和删除逻辑类似,删除时要注意:如果商品已经被订单引用,直接删会导致订单明细查不到商品名。常见做法是软删除,把status改为 0 而不是物理删除。
4.3 订单管理与状态流转
后台订单列表从orders表查,关联user表拿用户名。订单状态一般用数字表示:0 待付款、1 已付款、2 已发货、3 已完成、4 已取消。管理员可以修改状态:
<?php $orderId = intval($_POST['order_id']); $newStatus = intval($_POST['status']); $allowStatus = [0, 1, 2, 3, 4]; if (!in_array($newStatus, $allowStatus)) { die('非法状态'); } $stmt = $conn->prepare("UPDATE orders SET status = ? WHERE id = ?"); $stmt->bind_param('ii', $newStatus, $orderId); $stmt->execute(); header('Location: order_list.php'); ?>状态流转要有约束,比如已完成的订单不能再改回待付款。可以在更新前先查当前状态,判断是否允许这次变更。课程设计里不做这么细也能过,但答辩时老师可能会问「如果用户已经付款了,管理员误点取消怎么办」,提前想好这个问题的答案。
5. 避坑与排查:从连接失败到中文乱码的五个血泪经验
5.1 数据库连接报 error 2002
现象:PHP 页面报error 2002 (HY000): can't connect to local MySQL server through socket '/tmp/mysql.sock'。
原因:PHP 配置里mysqli.default_socket指向的路径和 MySQL 实际 socket 路径不一致,或者 MySQL 服务根本没启动。
解决:先systemctl status mysql确认服务在跑。如果服务正常,把连接配置里的localhost改成127.0.0.1,强制走 TCP 而不是 socket。或者找到 MySQL 真实的 socket 路径,在php.ini里设置mysqli.default_socket = /var/run/mysqld/mysqld.sock。
5.2 中文商品名显示为问号或乱码
现象:数据库里中文正常,但页面上显示???或æœ?è£?。
原因:字符集在三个环节中有一个不一致——数据库建库字符集、表字符集、PHP 连接字符集。
解决:建库时用utf8mb4,连接后立即执行$conn->set_charset('utf8mb4'),HTML 头部加<meta charset="utf-8">。三处统一后乱码消失。如果数据已经乱码,需要重新导入 SQL 文件,导入时指定--default-character-set=utf8mb4。
5.3 图片上传成功但页面不显示
现象:uploads/目录里能看到图片文件,但商品列表页图片是裂图。
原因:数据库里存的路径是相对路径,但页面所在的目录层级不同,相对路径解析错误。
解决:统一用相对于网站根目录的路径,比如/uploads/goods_xxx.jpg,前面加斜杠。或者在模板里用__DIR__或$_SERVER['DOCUMENT_ROOT']拼绝对路径。检查浏览器控制台的 Network 面板,看图片请求的 URL 是什么,就能定位路径问题。
5.4 后台页面报 headers already sent
现象:登录成功后跳转报Warning: Cannot modify header information - headers already sent。
原因:header()函数之前有输出,哪怕是一个空格或 BOM 头。
解决:检查<?php之前有没有空行或空格,PHP 文件保存时选「UTF-8 无 BOM」格式。session_start()和header()必须放在文件最顶部,前面不能有任何 HTML 或 echo 输出。
5.5 下单时库存扣成负数
现象:两个用户同时买最后一件商品,库存变成 -1。
原因:没有用事务和行级锁,两个请求同时读到库存为 1,都判断通过,都执行了扣减。
解决:用SELECT ... FOR UPDATE在事务内锁定行,或者用UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ?这种带条件的更新,检查affected_rows是否为 1,为 0 说明库存不足。
6. 二次开发与验证:用一条 SQL 确认数据一致性
跑通之后,怎么验证这套系统的数据是可靠的?我一般会写一条对账 SQL,检查订单总金额和订单明细汇总是否一致:
-- 检查订单主表总价与明细汇总是否一致 SELECT o.id, o.order_no, o.total AS order_total, SUM(oi.price * oi.num) AS items_total FROM orders o LEFT JOIN order_items oi ON oi.order_id = o.id GROUP BY o.id HAVING ABS(o.total - IFNULL(SUM(oi.price * oi.num), 0)) > 0.01;这条 SQL 的逻辑是:按订单分组,把明细表里每条商品的「单价 × 数量」加起来,和订单主表的total对比。如果差值的绝对值大于 0.01(浮点数精度容差),说明这单数据有问题。正常情况下应该返回空结果集。如果查出记录,就去检查下单时的事务逻辑,看是不是某次扣减或插入失败了。
二次开发时,最常见的需求是加一个「商品搜索」功能。在goods表上建索引,然后改查询:
ALTER TABLE goods ADD INDEX idx_name (name);<?php $keyword = trim($_GET['keyword'] ?? ''); if ($keyword !== '') { $like = '%' . $keyword . '%'; $stmt = $conn->prepare("SELECT id, name, price, img FROM goods WHERE status = 1 AND name LIKE ? ORDER BY id DESC LIMIT 20"); $stmt->bind_param('s', $like); $stmt->execute(); $result = $stmt->get_result(); } ?>LIKE '%关键词%'会导致索引失效,但课程设计数据量小,性能影响可以忽略。如果数据量上万,就要换成全文索引或搜索引擎。
还有一个容易忽略的点:config.php里的数据库密码不要提交到公开仓库。我习惯在项目根目录加一个.gitignore,把config.php排除掉,同时提供一个config.example.php模板。这样别人拿到源码后复制一份改密码就能跑,不会泄露自己的数据库信息。
从那以后我每次拿到一个 PHP 项目,都强制先跑一遍「导入 SQL → 改连接配置 → 访问首页 → 走一遍下单流程」这四步,确认数据链路通了再去看代码细节。希望帮到你。
本文还有配套的精品资源,点击获取