做电商项目这些年我越来越觉得从零搭一套商城系统是检验PHP基本功最好的方式。最近拿到一套名为木兮的服装购物系统源码文件名后面带着编号38169应该是打包发布时记录的版本号。这套系统用原生PHP加MySQL写成前台有商品浏览、规格选择、购物车、订单结算后台有商品管理、分类管理、订单管理、会员管理功能完整度在同类源码里算比较高的。这篇文章不打算照着官网说明念而是站在一个接手源码的开发者角度把木兮从架构到数据库、从前台流程到后台操作、从安全加固到二次开发一条条拆开讲清楚。无论你是刚学完PHP想找个完整项目练手还是手里已经有这套源码正打算改造成自己的服装商城都能从中找到可以直接抄作业的内容。1. 拆解整体架构原生PHP项目的目录组织与双入口设计1.1 前台与后台的双入口机制打开木兮的源码包第一眼看到的结构通常是根目录下放着index.php和admin.php两个入口文件分别对应前台购物端和后台管理端。这种双入口设计在早期PHP项目里非常普遍好处是简单直接访问前台就是/index.php访问后台就是/admin.php不需要复杂的路由配置放到任何一个支持PHP的虚拟主机上都能跑。我当时第一次看到这个结构就觉得这套代码的受众定位非常明确就是给那些不想折腾框架、希望拿来就能部署的开发者用的。与之配套的目录结构一般长这样/ ├── index.php // 前台入口 ├── admin.php // 后台入口 ├── config/ │ └── database.php // 数据库连接配置 ├── includes/ │ ├── init.php // 初始化DB、session、公共函数 │ └── common.php // 公共函数库 ├── template/ │ ├── home/ // 前台模板 │ └── admin/ // 后台模板 ├── upload/ // 商品图片上传目录 └── install/ // 安装向导脚本init.php是整套系统的地基它负责三件事用mysqli建立数据库连接、调用session_start()开启会话、把common.php里的公共函数加载进来。所有业务页面在干活之前都会先require_once这个文件。这样的好处是任何页面都能直接使用$db对象和公共函数不用重复写连接代码。1.2 为什么这类项目偏爱原生PHP而不是框架这里要解释一个很多新手困惑的问题现在主流PHP开发都用Laravel、ThinkPHP这类框架为什么木兮这类下载源码仍然坚持原生PHP我拆完之后的理解是原生PHP项目有三个不可替代的优势。第一部署门槛极低。框架项目往往需要Composer装依赖、配置伪静态规则、设置runtime目录权限而原生项目只需要把源码丢到网站根目录导入一份SQL改一下数据库连接文件就能跑起来。对于很多只是想要一个能用的商城、又不太熟悉服务器操作的小团队来说这才是最省事的选择。第二代码完全透明。框架把大量逻辑隐藏在最外层封装里出问题要顺着命名空间一层层翻源码。原生项目里一个goods_list.php从查数据库到输出HTML都是明摆着的改起来胆子大不容易改崩。第三资源占用低。原生PHP不需要加载框架的常驻内核同样的虚拟主机配置原生项目能承受的并发和响应速度通常更好。当然代价也有就是路由不灵活、代码重复多、团队协作时规范难统一真到了业务量上来的时候重构成本会很高。1.3 配置项与公共函数库的阅读重点接手任何一套源码我建议先看两个文件一个config/database.php一个includes/common.php。前者能告诉你数据库连接参数怎么配后者能告诉你这个项目的代码习惯是什么。木兮的公共函数库大致围绕几个职责展开SQL查询的封装函数、表单提交的判断、上传文件处理、分页计算。比如封装一个query($sql)函数去执行查询并返回结果集比每个页面都直接写mysqli_query要集中好维护。阅读这套源码的时候你会发现很多页面几行代码就能完成一块功能原因就是公共函数把琐碎逻辑都吃掉了。提示拿到源码后第一件事不要急着配置数据库先把common.php里所有函数名和注释扫一遍。理解了这些公共函数整个系统的调用关系就清晰了一大半。2. 数据库设计服装商品的多维属性如何落到表里2.1 分类表与商品表一对多的核心关系服装购物系统和普通商城最大的区别在于商品属性维度多颜色、尺码、季节、款式都要管理。但木兮这类定位偏轻量的系统不会一上来就上SKU库存量单位多级模型而是先用最基础的两张表把商品主体撑起来。分类表category通常包含cat_id、cat_name、parent_id、sort_order这几个字段。parent_id用来支持二级分类比如女装下面是连衣裙T恤前台可以通过递归或循环把二级分类拉出来。商品表goods的核心字段是goods_id、goods_name、cat_id、price、original_price、stock、goods_image、goods_desc、is_on_sale、add_time。这里我想强调一个设计细节价格字段建议直接用DECIMAL(10,2)而不是FLOAT。浮点数在计算总价时会出现0.10.2不等于0.3的精度问题这在订单金额上是要命的。我见过不少半路改造的项目因为价格字段用错类型导致前后台金额对不上的。2.2 颜色尺码与库存从单表字段到SKU表服装商品绕不开颜色和尺码的规格选择。最简单的做法是在goods表里加两个字段color和size用逗号分隔可选值比如color存黑色,白色,藏青前台展示成单选按钮。这种方案做起来很快但库存只能按整件商品算没办法精确到黑色XL码还剩5件。稍微进阶一点的做法是加一张goods_sku表每个规格组合一条记录字段包括sku_id、goods_id、spec_value、price、stock。spec_value用约定好的文本格式存比如颜色:黑色|尺码:XL这样在订单里记录商品快照时可以直接存成字符串不用频繁联表。前台用户选定某个规格组合后商品详情页通过AJAX查询对应SKU的库存和价格把实时数据刷出来。木兮这一类源码多数处于单表字段和SKU表之间的过渡状态。如果是拿来跑业务我强烈建议至少把SKU表补上否则服装店没法解决一个款式多个尺码分别管理库存这个核心诉求。2.3 订单表与订单商品表一张主表加一张子表订单部分的设计直接决定这套系统的严谨程度。常规做法是拆成orders和order_goods两张表。orders表记录订单整体信息order_id、order_sn、user_id、total_amount、pay_status、shipping_status、order_status、add_time。order_goods表记录订单里的每件商品快照id、order_id、goods_id、sku_id、goods_name、spec_info、price、number。为什么要在子表里冗余存一份goods_name和price而不是下单时只存商品ID因为商品以后可能改价、改名、甚至下架如果订单只是引用商品表历史订单显示的信息就会跟着变这对财务对账是不可接受的。订单表里保存商品快照是电商系统的基本原则之一。订单状态一般用一个TINYINT字段表示比如0待付款、1待发货、2待收货、3已完成、4已取消。状态的含义在代码里最好用常量数组统一维护避免到处写魔法数字。order_sn是订单号建议生成规则包含日期和随机串例如date(YmdHis) . rand(1000, 9999)这样既好看又不容易撞号。2.4 用户表、收货地址表与购物车表用户模块相对简单user表保存user_id、username、password、mobile、email、reg_time。这里必须提醒一句密码存储不要用明文也不要只做简单的MD5。至少要用password_hash()配合password_verify()来做如果这套源码还在用md5($password)接手之后第一件事就是把它换掉。收货地址表user_address记录address_id、user_id、consignee、mobile、province、city、district、address、is_default。省市区三字段建议用文本存储省得联行政区划表能精确到街道地址就够了别过度设计。购物车表cart要兼容游客场景所以既要有user_id登录用户也要有session_id游客标识。字段包括cart_id、user_id、session_id、goods_id、sku_id、number。当用户登录后可以把当前session_id下的购物车数据合并到这个用户下这一步在用户点击登录成功的回调里处理。3. 前台核心链路从浏览商品到成功下单的完整流程3.1 商品列表页分类筛选、分页与排序的实现思路前台首页和列表页是用户接触系统的第一站。列表页的典型URL是index.php?actlistcat_id2page1通过$_GET[cat_id]接收分类ID拼SQL时用WHERE限定分类。这里有个容易出问题的点如果只是WHERE cat_id 2那二级分类下的商品在一级分类列表里就不会显示。简单可用的解法是把这个分类下面的所有子分类ID查出来拼成一个IN (2, 5, 6)。分页逻辑一般是当前页$page每页数量$pageSize通过LIMIT ($page-1)*$pageSize, $pageSize实现。要算总页数就得先执行一条SELECT COUNT(*)的查询再ceil($total / $pageSize)得到页数。分页函数建议抽到common.php里返回一个包含数据和分页HTML的数组这样列表页代码能干净很多。排序维度通常支持三个按上架时间、按销量、按价格。实现时千万不要用ORDER BY $_GET[sort]直接拼SQL因为排序字段一旦被外部控制很容易被构造出恶意SQL。正确做法是用白名单数组限制允许的字段非法值回退到默认排序。3.2 商品详情页规格切换与加入购物车商品详情页是转化率的关键。前台打开index.php?actdetailgoods_id18后页面要做三件事根据goods_id查商品主信息、查该商品下的SKU列表、查商品相册图片。规格切换这块前端通常这么配合页面加载时把所有可选的规格组合连同价格、库存作为JSON输出到隐藏节点用户点击黑色XL这些按钮时用JavaScript判断当前组合是否合法、库存是否大于0并在界面直接显示库存不足的提示。这样做的好处是不用每次切换都发一次AJAX请求用户体验流畅对服务器的压力也小。加入购物车本质是一次表单提交传递goods_id、sku_id、number。后端处理逻辑要先做库存校验再判断用户是否登录。已登录用户写入cart表时填user_id游客则填session_id。这里我建议做一层统一封装不管登录没登录购物车的读取逻辑都优先用user_id没有就用session_id兜底。3.3 购物车页面与结算页的数据组装购物车列表页的数据组装是典型的多表联查场景。一条SQL把三张表串起来cart关联goods拿商品标题和图片关联goods_sku拿规格描述和当前SKU价格。计算总价时在PHP循环里逐条用price * number累加而不是在SQL里SUM因为商品单价来自SKU表直接在SQL里SUM容易把规格价格搞错。结算页要选的收货地址从user_address表里取默认地址排序靠前。如果没有地址要引导用户先新增。确认订单时把商品清单、地址信息、总金额在一个页面集中展示用户确认后提交订单。这一步是整个流程里编码最重的地方我放到下一节专门说。3.4 提交订单的事务处理库存校验与防超卖提交订单按钮一按后端做的事情比想象中多得多。为了不出错整个操作必须放进数据库事务里mysqli_begin_transaction($db); try { // 1. 查出购物车中所有商品及SKU // 2. 逐条校验库存是否充足不足则抛出异常 // 3. 插入 orders 主表拿到 order_id // 4. 循环插入 order_goods 子表 // 5. 逐条 UPDATE goods_sku SET stock stock - number // 6. 清空购物车 mysqli_commit($db); } catch (Exception $e) { mysqli_rollback($db); // 跳回结算页并提示 }这里最关键的是先校验库存再更新库存的顺序。如果不加控制两个用户同一时间下同一件商品的订单就可能都通过库存校验最后把库存扣成负数这就是超卖。简单项目可以这样优化校验库存后马上执行UPDATE goods_sku SET stock stock - number WHERE sku_id ? AND stock number如果受影响行数为0说明库存已经不够直接回滚。这一步把校验扣减合并成了原子操作能挡住绝大多数并发问题。注意事务里的每一步都要用受影响的记录数和异常机制做严格判断任何一步失败都要回滚。曾经有人图省事跳过事务线上出现订单生成了、库存没扣、购物车也没清的状态对账时直接懵掉。4. 后台管理实操商品上架、图片上传与订单发货4.1 管理员登录鉴权后台入口admin.php要先检查管理员是否登录未登录就跳转到登录页。管理员表admin单独建不跟前台用户混在一起。登录成功后把admin_id和admin_name写入session后台每个页面顶部都做一次session检查防止绕过登录直接访问。这里要提醒一个常见漏洞有些源码把登录校验只写在菜单页而具体的功能处理页忘记了攻击者可以通过直接构造URL调功能。接手后建议把后台所有页面的执行入口都统一收口到一个admin_init.php这个文件做鉴权其余页面全部引用它从架构上堵住漏洞。4.2 商品管理表单、上传与缩略图后台商品管理的核心是新增商品和编辑商品两个表单页字段覆盖商品名、分类、价格、原价、库存、商品图、描述、上下架状态。保存逻辑要判断是新增还是编辑新增执行插入编辑执行更新更新时不能改goods_id。商品图片上传是后台最常见的功能处理流程要覆盖四步检查上传错误码、检查扩展名白名单、生成随机文件名、移动到upload目录。扩展名白名单只允许jpg、jpeg、png、gif文件名用uniqid()加原始扩展名拼出来避免用户上传中文名文件导致路径问题。缩略图通常可选做。像木兮这类原生项目如果配置了GD库可以用imagecreatefromjpeg和imagecopyresampled生成一张列表页专用的小图避免列表页加载原图。实测下来100张原图每张几百K的页面换成缩略图后加载速度能提升一个量级。4.3 订单管理状态筛选与发货操作后台订单列表默认按状态筛选对应前台订单状态流转后台操作也同样用整数字段控制。待发货订单点发货时表单录入物流公司和运单号保存到orders表的shipping_company、shipping_sn字段同时把订单状态从待发货改成待收货。订单详情页要把order_goods里的商品快照、收货地址、订单金额全部展示出来最好还加一块订单操作日志记录什么时间谁做了什么操作。虽然会增加一张日志表但对排查售后纠纷帮助巨大属于成本低、收益高的功能值得优先开发。4.4 简易数据看板后台首页一般放几个统计数字今日订单数、今日销售额、待发货订单数、库存预警商品数。这些用聚合SQL即可比如SELECT COUNT(*), SUM(total_amount) FROM orders WHERE add_time CURDATE() AND order_status NOT IN (4)。现在看着简单但能让运营人员每天开机先看到业务大盘日常管理就缺不了这块。销售走势图属于加分项可以按天分组查订单表把数据输出成JSON给前端图表。5. 安全加固与上线部署源码项目最常见的几个坑5.1 SQL注入拼接查询与参数绑定之别原生PHP项目最容易出问题的就是SQL拼接。很多早期源码写的是$sql SELECT * FROM goods WHERE goods_id . $_GET[id]这种写法一旦遇到id1 OR 11整张表的数据都能被拖出来。修复思路分两个层次。最低限度是在每个进入SQL的变量上做转义传统写法用mysqli_real_escape_string()包裹变量虽然能挡住大部分注入但仍然不够优雅。更推荐的方式是改造查询封装让业务代码尽量用预处理语句。PDO的预处理写法对新手最友好$stmt $pdo-prepare(SELECT * FROM goods WHERE goods_id ?); $stmt-execute([$_GET[id]]); $goods $stmt-fetch();把关键查询改造成这种模式注入面就基本关死了。改造量可以接受核心是商品查询、订单查询、用户查询这几处高频点。5.2 XSS与CSRF防护前台所有来自数据库又需要展示到HTML的内容比如商品名、商品描述、分类名输出前都要过一遍htmlspecialchars()防止XSS。如果是富文本编辑器的内容需要更细致的处理至少要限制script标签和javascript:协议头的URL。后台表单建议加CSRF Token校验。实现方式就是在生成表单时写入一个随机token到session同时放到隐藏字段提交时比对两者是否一致。这个机制能有效阻止跨站请求伪造防止有人诱导管理员提交恶意操作。别嫌麻烦管理员的权限太高一旦被利用损失是直接的。5.3 文件上传漏洞只检查后缀远远不够后台商品图上传如果只检查扩展名攻击者完全可以改个名字上传一个带PHP代码的文件。完整的上传校验至少要有三重限制文件扩展名白名单、MIME类型检查、上传目录禁止执行脚本。上传目录禁止执行脚本这一点Apache下可以在upload目录里放.htaccess内容是php_flag engine offNginx下则在location配置里不解析该目录的PHP请求。实测下来很多被人拿到shell的PHP商城都是栽在上传目录没有做执行限制上这一条必须在部署时落实。5.4 部署到服务器时的环境适配原生PHP项目部署主要看三样PHP版本、数据库驱动、伪静态规则。接手老源码最典型的问题是代码里还在用mysql_connect这个函数PHP7.0就移除了必须统一改成mysqli或PDO_MySQL。数据库连接字符集要设置为utf8mb4否则商品描述里遇到生僻字或表情符号会直接变问号。如果部署在Nginx环境入口文件是index.php只需要配置好try_files $uri $uri/ /index.php?$query_string;即可后台入口同理。生产环境记得在php.ini里关闭display_errors打开错误日志不然SQL报错信息会直接暴露表结构给攻击者递刀。6. 二次开发指南把木兮改成你自己的服装商城6.1 换肤与页面改造拿到源码先改外观是最直观的诉求。由于模板层和业务逻辑没有完全分离换肤时需要同时调整HTML结构和CSS两个部分。建议做法是保留原来的页面输出结构只替换CSS文件如果要大幅改HTML则对照模板里输出的变量名把新HTML里的对应位置套进去即可。新增字段相对要麻烦一点比如想在商品表加一个材质字段顺序是先在数据库goods表加material字段再到后台商品添加和编辑表单加输入框再到前台详情页输出。三个地方一起改缺一个都会出现数据存了却看不到的问题。6.2 接入微信支付或支付宝支付的思路支付对接是服装商城必须补的功能。如果要接入核心链路是用户提交订单后生成order_sn跳转到支付收银台支付完成后第三方服务会把回调请求发到你的服务器回调里验签通过后更新订单状态。重点要处理好回调的幂等性也就是同一个订单的支付通知可能会收到多次必须判断订单当前状态只有待付款状态才更新为已付款避免重复处理。需要准备商户号、应用私钥、平台公钥、回调URL这些配置项统一放在一个payment_config.php文件里别散落在各处。6.3 性能优化与功能演进方向如果担心网站访问量大起来会撑不住可以从三个方向递进优化。第一层是给常用的商品详情查询加缓存项目刚起步时可以用文件缓存或apc数据量大了再换Redis。第二层是给goods表的cat_id、goods_id和orders表的order_sn、user_id加上索引很多商城变慢并不是PHP慢而是全表扫描。功能上值得扩展的优先级也很明确优惠券功能需要加coupon和user_coupon两张表秒杀活动需要给商品加活动价和活动库存字段搜索功能可以先用LIKE %关键词%顶一阵真正做到一定规模后再考虑接入专业搜索服务。我在实际拆解和改造这套源码的过程中最大的感受是原生PHP购物系统的价值不在代码有多花哨而在于逻辑直白能让人快速建立电商业务的全景认知。如果你也是第一次接触这类项目建议按数据库→公共函数→前台流程→后台流程的顺序去读把订单那条链路彻底吃透其他的都是围绕它展开的。最后再分享一个小技巧改造前先在本地用Git把原始源码提交一个版本这样每次改动都能对比回退比任何备份习惯都靠谱。