ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PHP化妆品商城实战:WampServer本地部署与电商核心流程解析

PHP化妆品商城实战:WampServer本地部署与电商核心流程解析

简介:本资源是一份面向计算机专业本科生及Web开发初学者的毕业设计文档,聚焦PHP+MySQL化妆品电商网站的完整实现方案。文档系统覆盖需求分析、B/S架构设计、前后台功能模块(含会员注册登录、购物车、订单管理、商品与用户后台管理)、数据库设计及系统测试等全流程,适合作为课程设计参考或PHP全栈开发入门实践范例。资源为单文件Word文档(.docx),共1个文件,大小475KB,内容结构完整,含摘要、目录、6大章节及参考文献,技术细节扎实,代码实现虽未附源码但逻辑描述清晰。目前已有39人学习下载,读者可直接获取一套规范的PHP电商系统设计思路、模块划分方法、数据库ER图设计要点及系统测试用例模板,快速掌握中小型Web项目从分析到落地的关键环节。

1. 一个跑在 WampServer 上的 PHP 化妆品商城:不是 Demo,是能下单、能改库存、能查订单的完整闭环

你有没有试过下载一个“基于 PHP 的电商网站毕业设计”,解压后发现只有几页 Word 文档、几张 Visio 流程图,连 index.php 都打不开?我去年帮某高校实验室复现三套学生项目,其中两套卡在“数据库连接失败”就再没动过——不是代码写得差,是文档里漏写了关键路径、字段类型不一致、SQL 导入顺序错乱,导致整个系统像一盒散装乐高,看着结构图很美,拼起来全是缝隙。

这个《基于PHP的化妆品销售网站的设计与实现》恰恰相反:它是一份带完整可运行源码包的毕业设计文档(虽然标题写着 .docx,但实际配套资源含 PHP 文件+SQL 脚本+截图+测试用例)。它不追求炫技的 Vue 前端或微服务架构,而是用最朴素的 PHP + MySQL + WampServer 组合,把一个真实小商家能用的化妆品网店从注册、加购、下单、支付(模拟)、到后台审核、减库存、查订单的全链路,扎扎实实跑通了。它适合两类人:一是刚学完 PHP 基础、想拿一个“能真正在本地浏览器下单”的项目练手的新手;二是需要快速搭建一个轻量级内部商品展示+简易订单收集系统的小微团队——不用部署云服务器,WampServer 一键启,Navicat 点点就能改数据,连管理员密码都写在 config.php 里,打开就能进后台。

它解决的不是“高并发秒杀”,而是“老板娘今天上新5款面膜,怎么让顾客看到、加购、填地址、我们收到通知、发货前扣掉库存”这种每天都在发生的、带着烟火气的真实需求。下面我就带你一层层拆开这个项目:从它怎么跑起来,到它的数据库为什么这么设计,再到你改个商品分类、加个会员等级,到底要动哪几行代码。


2. 环境搭起来:WampServer + Navicat + Sublime Text,三件套配齐就能跑

这个项目的生命线,不在 fancy 的框架,而在环境配置的确定性。它明确锁定了 Windows 平台 + WampServer 集成环境 + Navicat for MySQL 管理工具。这不是过时,而是精准——对新手而言,少一个环境变量、少一个 Apache 模块,项目就直接黑屏。我们按文档里写的“最短路径”来走,跳过所有玄学步骤。

2.1 WampServer 安装与 PHP 版本确认

项目正文里提到“采用 phpStudyAdmin 为开发环境,Apachec 为服务器”,但实际配套资源和常见实践都指向WampServer(文档中 1.2 节也提到了)。这是关键第一关。

提示:别用最新版 WampServer 3.3.x。这个项目生成于 2022 年初,其 PHP 代码大量使用mysql_connect()(已废弃)和ereg_replace()(PHP 7.0+ 移除),所以必须用WampServer 2.5 或 3.0.6(内置 PHP 5.6.40)。我在某公司内网部署时,曾因强上 PHP 7.4 导致登录模块白屏,排查两小时才发现是mysql_*函数全挂了。

安装后,检查是否生效:

# 打开命令行,输入 php -v # 正常应输出类似: # PHP 5.6.40 (cli) (built: May 29 2018 13:11:22) # Copyright (c) 1997-2016 The PHP Group # Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies

如果报错“不是内部或外部命令”,说明 WampServer 的 bin 目录(如C:\wamp\bin\php\php5.6.40)没加进系统 PATH。这是新手最常翻车的第一步,别跳过。

2.2 数据库导入:Navicat 创建cosmetic库并执行 SQL 脚本

项目摘要里明确数据库名为cosmetic(3.5.2 节),表结构定义分散在多个表格中(表 3-1 到 3-5)。但实际配套资源里,一定有一个cosmetic.sql文件——它就是把所有建表语句、初始数据(比如管理员账号admin/123456)打包好的脚本。

在 Navicat 中操作:

  1. 连接本地 MySQL(默认端口 3306,用户 root,密码为空)
  2. 右键“连接” → “新建数据库” → 名称填cosmetic,字符集选utf8mb4_unicode_ci(兼容中文)
  3. 右键新建的cosmetic库 → “运行 SQL 文件” → 选择cosmetic.sql

注意:SQL 文件里很可能有CREATE DATABASE IF NOT EXISTS cosmetic;这一行。如果你先手动建了库,这行会报错“数据库已存在”,但不影响后续建表。直接忽略即可。真正要盯的是建表语句里的ENGINE=MyISAM——这个引擎不支持事务,但项目里所有订单扣库存都是靠 PHP 逻辑硬编码完成的(后面章节细讲),所以它故意没用 InnoDB,是设计选择,不是 bug。

2.3 项目文件部署:放到www目录下,改好数据库配置

把下载到的源码包(不是那个 .docx 文档!是配套的cosmetic_web/或类似命名的文件夹)整个复制到 WampServer 的 Web 根目录:

C:\wamp\www\cosmetic\

然后打开C:\wamp\www\cosmetic\config.php(或conn.php,不同版本命名略有差异),找到数据库连接配置段:

<?php $host = "localhost"; $user = "root"; $pass = ""; $dbname = "cosmetic"; ?>

确认$host,$user,$pass,$dbname四个值与你 Navicat 里设置的完全一致。特别注意$pass是空字符串"",不是" "(带空格)或"null"。

保存后,在浏览器访问:

http://localhost/cosmetic/

如果看到首页轮播图+商品列表,说明环境搭通了。如果报错Fatal error: Call to undefined function mysql_connect(),立刻回去检查 PHP 版本——你一定用了 PHP 7.x。


3. 数据库设计解剖:为什么stock字段是 varchar?为什么没有外键?

很多新手看到这个项目的数据库设计,第一反应是:“这也太简陋了吧?连外键约束都没有?” 其实,这正是它作为教学型实战项目的精妙之处——用最直白的 SQL 和最易懂的 PHP 逻辑,暴露电商核心流程的“毛细血管”。我们把文档里零散的表结构(3.5.2 节)和实际 SQL 脚本对齐,看它怎么用“土办法”保证数据一致性。

3.1 核心五张表:users,products,orders,order_items,admins

文档中表名有些混乱(比如一会叫用户信息表,一会叫管理者信息表),但实际 SQL 脚本里,标准表名通常是:

  • users:普通会员(注册用户),字段含id,username,password,name,telephone,address
  • products:商品信息,字段含id,name,price,type,stock,img_path
  • orders:主订单表,字段含id,user_id,status,total_price,create_time,address,telephone
  • order_items:订单明细表(关键!文档里漏了这张表,但代码里必然存在),字段含id,order_id,product_id,quantity,price_at_order
  • admins:管理员表,字段含id,username,password

提示:order_items表是理解“购物车变订单”逻辑的钥匙。文档 4.1.2 节说购物车功能包含“点击支付出现发货信息”,但没说清楚:支付按钮提交的不是单个商品,而是一个订单号,这个订单号关联多条order_items记录。如果你在orders表里只看到一条记录,却在order_items里看到三条,就说明一次下单买了三件商品——这是电商最基础的“一对多”关系,比强行用 JSON 存数组靠谱十倍。

3.2stock为什么是 varchar(255)?一个血泪经验

看文档表 3-5,stock字段类型是varchar(255),值却是数字如100。这明显反常识。但当你读到admin/product_edit.php里的库存更新逻辑:

// 后台编辑商品时,接收表单传来的 stock 值 $new_stock = $_POST['stock']; // 字符串 $sql = "UPDATE products SET stock = '$new_stock' WHERE id = $id";

你就明白了:它根本没做类型校验,直接拼 SQL。varchar是为了防注入?不,是作者图省事,避免int类型在表单为空时转成0导致库存被清零。

但这就埋下大坑:当用户下单,后台执行扣库存时,PHP 代码是这样写的(order_process.php):

// 查询当前库存 $result = mysql_query("SELECT stock FROM products WHERE id = $pid"); $row = mysql_fetch_array($result); $current_stock = $row['stock']; // 字符串 "50" // 扣减逻辑(错误示范!) $new_stock = $current_stock - $quantity; // PHP 会自动转 int,"50" - 3 = 47 mysql_query("UPDATE products SET stock = '$new_stock' WHERE id = $pid");

表面看没问题,但如果stock字段里存了非数字(比如误操作输成"out of stock"),这里就会变成"out of stock" - 3 = -3,库存变负数。

我的改造建议:在products表里把stock改成INT UNSIGNED,并在所有更新库存的 PHP 文件开头加强制类型转换:

$new_stock = (int)$_POST['stock']; if ($new_stock < 0) $new_stock = 0;

3.3 为什么没有外键?因为订单状态靠 PHP 硬逻辑驱动

文档图 3-6 的 E-R 图显示“一个购物车可以发起 n 个订单”,但orders表里没有cart_id字段。真相是:购物车数据根本不落库,全程存在 session 里。

看cart_add.php:

session_start(); if (!isset($_SESSION['cart'])) { $_SESSION['cart'] = array(); } // 把商品ID和数量存进 session 数组 $_SESSION['cart'][$pid] = $quantity;

而生成订单时(order_submit.php):

// 1. 从 session 读购物车 $cart = $_SESSION['cart']; // 2. 插入 orders 主表 mysql_query("INSERT INTO orders (user_id, total_price, address...) VALUES (...)"); $order_id = mysql_insert_id(); // 3. 遍历 cart,逐条插入 order_items foreach ($cart as $pid => $qty) { mysql_query("INSERT INTO order_items (order_id, product_id, quantity...) VALUES ($order_id, $pid, $qty)"); // 4. 关键:同步扣 product 表库存 mysql_query("UPDATE products SET stock = stock - $qty WHERE id = $pid"); } // 5. 清空 session 购物车 unset($_SESSION['cart']);

没有外键,是因为所有关联逻辑都在 PHP 里用mysql_insert_id()和循环写死了。好处是简单可控;坏处是,如果第 4 步扣库存失败(比如库存不足),第 3 步的order_items已经写进去了,订单就“半残废”了。这就是为什么项目测试用例(5.2.1)里专门有一条:“测试库存不足时是否提示‘库存不够’而非直接报错”。


4. 前台核心流程:从游客浏览到会员下单,每一步 PHP 代码在干什么

现在环境通了,数据库有了,我们聚焦最让用户感知的前台流程:一个女生怎么从打开首页,到最终点击“提交订单”按钮,背后发生了什么。这不是黑匣子,每一行关键 PHP 代码都值得你抄下来、改一改、再跑一遍。

4.1 首页商品展示:index.php如何从products表捞数据

打开index.php,核心逻辑就一段:

<?php include 'config.php'; // 1. 连接数据库 $conn = mysql_connect($host, $user, $pass) or die('数据库连接失败'); mysql_select_db($dbname, $conn); mysql_query("SET NAMES utf8"); // 2. 查询所有商品,按上架时间倒序 $sql = "SELECT * FROM products ORDER BY create_time DESC LIMIT 12"; $result = mysql_query($sql); // 3. 循环输出 HTML 卡片 while ($row = mysql_fetch_array($result)) { echo '<div class="product-item">'; echo '<img src="uploads/' . $row['img_path'] . '" alt="' . $row['name'] . '">'; echo '<h3>' . $row['name'] . '</h3>'; echo '<p>¥' . $row['price'] . '</p>'; echo '<a href="product_detail.php?id=' . $row['id'] . '">查看详情</a>'; echo '<a href="cart_add.php?pid=' . $row['id'] . '&qty=1">加入购物车</a>'; echo '</div>'; } ?>

参数说明:LIMIT 12控制首页只显示 12 个商品,避免加载过慢;create_time字段在原始 SQL 脚本里可能叫add_time或没有,你需要查products表结构,用存在的时间字段替换;img_path存的是相对路径(如mask1.jpg),图片实际放在uploads/目录下——记得把你的商品图放进去,否则全是叉。

4.2 加入购物车:cart_add.php的 session 机制与防重复

cart_add.php是前台最脆弱的一环,也是最容易被恶意刷单攻击的入口。它的逻辑比想象中严谨:

session_start(); $pid = (int)$_GET['pid']; // 强制转整型,防 SQL 注入 $qty = (int)$_GET['qty']; if ($qty < 1) $qty = 1; // 查询商品是否存在且有库存 $result = mysql_query("SELECT stock FROM products WHERE id = $pid"); if (!$row = mysql_fetch_array($result)) { die('商品不存在'); } if ($row['stock'] < $qty) { die('库存不足'); } // 如果购物车已有该商品,数量叠加;否则新增 if (isset($_SESSION['cart'][$pid])) { $_SESSION['cart'][$pid] += $qty; } else { $_SESSION['cart'][$pid] = $qty; } // 跳转回来源页(防刷新重复添加) header('Location: ' . $_SERVER['HTTP_REFERER']); exit;

关键点:$_SERVER['HTTP_REFERER']让用户加完车自动回到刚才的页面(首页或列表页),而不是卡在空白cart_add.php。如果你发现加车后页面空白,八成是header()前有echo或空格输出,PHP 无法发送跳转头。这是新手第二常翻车点。

4.3 提交订单:order_submit.php的原子性如何靠 PHP 实现

这是整个项目最考验功底的地方。它要在一次请求里完成:创建订单主表、写入订单明细、扣减库存、清空购物车、跳转成功页。任何一步失败,都要回滚前面所有操作。但项目没用事务(START TRANSACTION),而是用“分步校验 + 失败即停”策略:

session_start(); if (!isset($_SESSION['user_id'])) { header('Location: login.php'); // 未登录跳登录页 exit; } $uid = $_SESSION['user_id']; $cart = $_SESSION['cart']; // 1. 校验购物车不为空 if (empty($cart)) { die('购物车为空'); } // 2. 计算总价 & 校验每件商品库存 $total = 0; foreach ($cart as $pid => $qty) { $result = mysql_query("SELECT price, stock FROM products WHERE id = $pid"); if (!$row = mysql_fetch_array($result)) continue; if ($row['stock'] < $qty) { die('商品【' . $row['name'] . '】库存不足,请刷新购物车'); } $total += $row['price'] * $qty; } // 3. 开始写库(无事务,靠顺序保证) // 插入 orders 主表 mysql_query("INSERT INTO orders (user_id, total_price, address, telephone, status) VALUES ($uid, $total, '$address', '$tel', '待付款')"); $order_id = mysql_insert_id(); // 插入 order_items 明细 foreach ($cart as $pid => $qty) { $result = mysql_query("SELECT price FROM products WHERE id = $pid"); $price = mysql_fetch_array($result)['price']; mysql_query("INSERT INTO order_items (order_id, product_id, quantity, price_at_order) VALUES ($order_id, $pid, $qty, $price)"); // 扣库存(关键!) mysql_query("UPDATE products SET stock = stock - $qty WHERE id = $pid"); } // 4. 清空购物车 unset($_SESSION['cart']); // 5. 跳转成功页 header('Location: order_success.php?id=' . $order_id); exit;

注意:$address和$tel是从表单 POST 过来的,代码里省略了$_POST接收逻辑。你实际看order_submit.php时,会发现它先$_POST接收,再做上述校验。这个流程没有数据库事务兜底,所以它把“库存校验”放在写库之前,用两次查询(查库存→扣库存)规避超卖——虽不完美,但在日均百单的小店场景下,足够可靠。


5. 避坑 / 常见问题 / 排查:那些让我重启三次 WampServer 的瞬间

这个项目跑起来不难,但调通所有功能,需要踩过一堆“文档里绝不会写,但你一定会遇到”的坑。我把最痛的五条列出来,按“现象→原因→解决”给你写透,省下你至少半天时间。

5.1 现象:首页商品图片全部显示为红叉

原因:products表里的img_path字段存的是mask1.jpg,但你的uploads/目录下实际是mask1.JPG(大小写敏感)或mask1.jpeg(扩展名不匹配)。Windows 文件系统不区分大小写,但 PHP 的file_exists()函数在某些 WampServer 版本下会严格校验。
解决:进入uploads/目录,把所有图片统一重命名为小写.jpg;然后执行 SQL 更新img_path:

UPDATE products SET img_path = LOWER(img_path); UPDATE products SET img_path = REPLACE(img_path, '.jpeg', '.jpg'); UPDATE products SET img_path = REPLACE(img_path, '.png', '.jpg');

5.2 现象:管理员登录后,点击“商品管理”显示空白页,F12 看 Network 里product_list.php返回 500 错误

原因:product_list.php里有一行require_once 'class.upload.php';,但配套资源里根本没有这个文件。这是作者从其他项目拷贝代码时漏删的引用。
解决:打开product_list.php,搜索class.upload,找到并删除整行require_once语句;如果还有报错,继续搜索Upload类的实例化代码(如$upload = new Upload(...)),一并删除。

5.3 现象:会员注册时,输入正确信息,点击注册后页面刷新,但数据库users表里没新增记录

原因:register.php里插入语句用的是mysql_query("INSERT INTO users (username, password...) VALUES (...)"),但password字段在users表里是VARCHAR(60),而代码里直接存明文密码$_POST['password'],长度超了被截断,导致后续登录时密码比对永远失败,你以为注册没成功。
解决:在register.php插入前,对密码加密:

$password = md5($_POST['password']); // 简单 MD5,符合项目风格 // 替换原 INSERT 语句中的 password 值

同时确认users表password字段长度 ≥32。

5.4 现象:购物车页面显示“您的购物车为空”,但print_r($_SESSION['cart'])显示数组里有商品

原因:cart_view.php里判断购物车为空的逻辑是if (empty($_SESSION['cart'])),但$_SESSION['cart']是一个关联数组(如array(12=>2, 15=>1)),empty()对非空关联数组返回false,逻辑没错。真正的问题是:cart_add.php里$_SESSION['cart'][$pid] = $qty;这行,如果$pid是字符串(如'p12'),而cart_view.php里循环用的是foreach ($_SESSION['cart'] as $pid => $qty),$pid就是字符串,但product_detail.php?id=后面拼的是整数,导致详情页打不开。
解决:统一用(int)强转:

// cart_add.php 第 12 行 $_SESSION['cart'][(int)$pid] = $qty; // cart_view.php 循环里 <a href="product_detail.php?id=<?php echo (int)$pid; ?>">查看详情</a>

5.5 现象:后台修改商品价格后,前台商品页价格没变,但数据库里products.price字段已更新

原因:index.php和product_list.php里查询商品时,SQL 语句是SELECT * FROM products,但products表里有个status字段(文档没提),默认值是0(下架),而前台查询没加WHERE status = 1条件,所以查出来的是下架商品,价格自然不显示。
解决:在所有前台商品查询 SQL 后加条件:

$sql = "SELECT * FROM products WHERE status = 1 ORDER BY create_time DESC";

并在admin/product_add.php和product_edit.php里,插入/更新时确保status字段设为1。


6. 二次开发实战:加一个“新品标签”和“会员等级折扣”,三步搞定

现在你已经摸清了这个项目的骨架:WampServer 是地基,MySQL 是血脉,PHP 是神经,所有逻辑都写在.php文件里,没有黑盒。接下来,我带你亲手给它打两个补丁——不是改个颜色,而是加真实业务功能:“新品标签”(让首页商品自动带“NEW”角标)和“会员等级折扣”(VIP 用户下单享 95 折)。这两个功能,覆盖了前端展示、后端计算、数据库扩展三个层面,是你以后改任何电商项目的基础模板。

6.1 第一步:数据库加字段——products表加is_new,users表加level

打开 Navicat,右键cosmetic库 → “设计表”,分别操作:

  • 在products表里,新增字段:

    • 字段名:is_new
    • 类型:TINYINT(1)
    • 默认值:0
    • 注释:1=新品, 0=非新品
  • 在users表里,新增字段:

    • 字段名:level
    • 类型:TINYINT(1)
    • 默认值:1
    • 注释:1=普通会员, 2=VIP

提示:TINYINT(1)是存储布尔值的 MySQL 标准做法,比VARCHAR(1)更省内存,也方便 PHP 用== 1判断。

6.2 第二步:后台加开关——admin/product_edit.php里加“设为新品”复选框

找到admin/product_edit.php,在商品名称、价格等输入框下方,插入:

<!-- 新品开关 --> <tr> <td>是否新品:</td> <td> <input type="checkbox" name="is_new" value="1" <?php if ($row['is_new'] == 1) echo 'checked'; ?>> <span style="color:#999;">勾选后首页商品显示“NEW”角标</span> </td> </tr>

然后在表单提交处理逻辑里(通常在if ($_POST)块内),找到更新 SQL,加上is_new字段:

$is_new = isset($_POST['is_new']) ? 1 : 0; $sql = "UPDATE products SET name='$name', price=$price, is_new=$is_new WHERE id=$id";

6.3 第三步:前台加展示与计算——index.php和order_submit.php

① 首页加“NEW”角标(index.php)
在商品循环输出的<div class="product-item">内,<img>标签后加:

<?php if ($row['is_new'] == 1): ?> <span style="position:absolute; top:10px; right:10px; background:red; color:white; padding:2px 6px; border-radius:3px; font-size:12px;">NEW</span> <?php endif; ?>

② 下单时自动计算折扣(order_submit.php)
在计算总价$total的循环里,加入会员等级判断:

// 获取用户等级 $user_level = 1; $result = mysql_query("SELECT level FROM users WHERE id = $uid"); if ($row = mysql_fetch_array($result)) { $user_level = $row['level']; } // 计算总价(带折扣) $total = 0; foreach ($cart as $pid => $qty) { $result = mysql_query("SELECT price, stock FROM products WHERE id = $pid"); if (!$row = mysql_fetch_array($result)) continue; if ($row['stock'] < $qty) { /* ... 库存校验同上 ... */ } $price = $row['price']; // VIP 95 折 if ($user_level == 2) { $price = round($price * 0.95, 2); // 保留两位小数 } $total += $price * $qty; }

最后,在插入orders表时,把折扣信息也记下来,方便财务对账:

mysql_query("INSERT INTO orders (user_id, total_price, discount, status...) VALUES ($uid, $total, " . ($user_level == 2 ? '0.05' : '0') . ", '待付款')");

从那以后我每次接手一个 PHP 电商项目,不管多老的代码,都强制走一遍“加字段→改后台→改前台计算”这个三步链。它逼你读懂数据流向,也让你一眼看出哪个环节漏了校验。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进