尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ThinkPHP5.0进销存系统实战:模块划分、部署与订单联动开发

ThinkPHP5.0进销存系统实战:模块划分、部署与订单联动开发 简介基于Thinkphp5.0框架开发的进销存客户管理系统源码面向中小企业及PHP开发者用于在线管理商品采购、销售、库存与客户资料适合快速搭建可二次开发的轻量业务后台。压缩包共2000个文件约15.13MB以php、html、js、css等前后端文件为主并包含png、gif等图片素材及sql数据库文件安装时导入demo_360.sql并修改数据库配置即可运行。系统内置后台与前台分离的登录机制后台默认账号admin前台账号由后台分配功能上覆盖商品、订单、库存、客户等核心管理模块同时提供报表、权限等辅助能力。已有117人学习浏览源码目录结构清晰配合注释与配置示例便于开发者理解Thinkphp5.0的路由、模型及模板用法也可直接作为进销存项目的起点进行二次开发。1. 一套能直接上线的ThinkPHP5.0进销存系统先看清楚它的边界很多小型企业处理进销存时最头疼的不是没有功能而是采购入库、销售出库和客户资料各记各的月底一对账才发现库存跟账面差出一大截。这套基于ThinkPHP5.0的进销存客户管理系统把商品、库存、订单、客户这几条线收拢到一套后台里前台跑业务后台做维护登录入口和管理员入口分开权限边界从一开始就划清楚。对正在选型的中小型销售业务来说它比从零开发起一个新系统要省下至少一半工作量对刚接触ThinkPHP5.0的开发者而言它也是一个能完整拆开的实战案例涵盖多模块划分、数据库设计、库存流水和订单联动。它的边界也很明确不追求大而全的ERP能力而是把采购到销售这条主链路做扎实让你在二次开发时不用重新搭地基。2. ThinkPHP5.0进销存系统的模块划分与数据建模2.1 框架选型为什么这类项目普遍使用TP5进销存业务最怕的是把所有逻辑塞进一个控制器里改成需求时无从下手。ThinkPHP5.0之所以在中型业务系统中出现频率高是因为它明确了请求生命周期URL通过路由解析到控制器控制器调用模型层操作数据库最后把数据交给视图渲染。这个单向流程和进销存里“单据触发数据变化”的场景天然匹配比如一张采购单入库后库存表和库存流水同时更新逻辑可以收拢到一个服务类里而不是散落在多个控制器。技术栈上TP5.0运行在PHP 5.6到7.2之间使用PDO连接MySQL。相比后面的ThinkPHP6TP5的目录结构更扁平应用目录下模块划分直观适合快速上手。这套源码保留了框架原始的application结构打开后能一眼分清后台、前台和公共层。对于需要维护老项目或者接手既有系统的工程师来说这种结构几乎没有学习成本。2.2 后台、前台与公共层如何分工打开源码根目录最需要注意的不是thinkphp核心包而是application下按模块分离的目录project_root/ ├── application/ # 应用目录 │ ├── admin/ # 后台管理模块 │ ├── member/ # 前台会员模块 │ ├── common/ # 公共模型和函数 │ ├── config.php # 应用基础配置 │ └── database.php # 数据库连接配置 ├── public_html/ # Web入口目录 │ ├── index.php # 单入口文件 │ └── static/ # 静态资源 ├── thinkphp/ # 框架核心目录 └── demo_360.sql # 数据库初始化备份这里有两个关键设计。第一入口收敛到public_html所有请求都通过index.php进入避免用户直接访问到application/database.php这类敏感文件。第二后台admin模块和前台member模块彼此独立各自维护登录会话后续做权限控制时可以在模块基类里统一加校验不用在业务方法中重复判断。common目录通常放一些跨模块的公共模型和辅助函数比如订单流水号生成、状态码映射、金额格式化。这个分层直接决定了改需求时的代价如果改动只涉及后台管理程序员只需要在admin模块内调整如果涉及库存变动规则则优先改common下的模型或服务类保证前台和后台调用的是同一套逻辑。2.3 核心数据表结构与关联关系进销存系统的数据核心不是某个表而是“库存变动”如何被记录。很多初版系统只在goods表里放一个stock字段销售时减一采购时加一看起来简单但碰上退货、盘点、订单取消数值会被改得不可追溯。这套系统的做法是建立独立的stock_log库存流水表把每一次数量变化都记录在案goods表里的stock只是用于列表展示的冗余字段。主要表结构如下表名作用关键字段goods商品主表goods_id, goods_name, category_id, stock, pricegoods_category商品分类表cid, name, parent_idorder订单主表order_id, order_sn, user_id, total_amount, statusorder_goods订单商品明细表id, order_id, goods_id, quantity, pricecustomer客户资料表customer_id, name, phone, address, levelstock_log库存变动日志表log_id, goods_id, type, quantity, before_stock, after_stock从关联关系上就能反推业务路径商品挂在分类下订单包含多条商品明细客户下订单产生消费记录而每一次订单确认或采购入库都会往stock_log里写一条变动日志。下面是库存日志表的建表语句重点在quantity的方向性CREATE TABLE stock_log ( log_id int(11) NOT NULL AUTO_INCREMENT, goods_id int(11) NOT NULL COMMENT 商品ID, type tinyint(1) NOT NULL COMMENT 1采购入库 2销售出库 3盘点调整, quantity int(11) NOT NULL COMMENT 变动数量正负表示方向, before_stock int(11) NOT NULL COMMENT 变动前库存, after_stock int(11) NOT NULL COMMENT 变动后库存, operator varchar(30) NOT NULL COMMENT 操作人或操作来源, create_time int(11) NOT NULL, PRIMARY KEY (log_id), KEY idx_goods_id (goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;quantity用正负号表示库存方向比单独维护一个direction字段更便于汇总当你要查某个商品在一段时间内的净变动直接SUM(quantity)就能得出结果。before_stock和after_stock看起来像是冗余实际作用是排查问题时能直接看到某次操作前后的库存快照不需要从头演算历史记录。商品主表和订单表在导入demo_360.sql后已经包含一定量的测试数据可以先用这些数据验证系统流程再根据需要清空重建。3. 安装部署从SQL导入、数据库配置到入口目录定向3.1 环境准备与导入demo_360.sql部署这套系统前需要先确认服务器环境满足以下条件PHP 5.6到7.2版本开启PDO MySQL扩展和GD图形库MySQL 5.7以上且支持utf8mb4字符集Web服务器使用Nginx或Apache均可。PHP版本过高时ThinkPHP5.0的某些写法会出现each函数被移除或count()参数类型变更导致的报错所以不建议在PHP 7.4以上版本直接运行。数据库初始化文件demo_360.sql位于项目根目录它包含完整的表结构和测试数据。命令行导入是最稳定的方式尤其是当SQL文件超过20MB时phpMyAdmin会经常超时mysql -u root -p -e CREATE DATABASE IF NOT EXISTS demo_360 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -u root -p demo_360 demo_360.sql第一条命令创建数据库并明确指定字符集。第二条命令把SQL文件导入到demo_360库。导入时如果遇到Unknown collation相关报错说明MySQL版本过低需要把SQL文件里的utf8mb4_unicode_ci批量替换成utf8mb4_general_ci或者升级数据库版本。导入完成后执行mysql -u root -p -e USE demo_360; SHOW TABLES;能看到goods、order、customer等表就说明成功。提示导入SQL前备份原始文件防止错误操作需要重新初始化。demo_360.sql里可能会带上演示环境的数据库前缀如果前缀和配置文件不一致后面登录时会报数据表不存在。3.2 修改application/database.php配置数据库连接信息集中在application/database.php这是ThinkPHP5.0的标准配置文件。打开文件后只需要修改以下几个字段return [ type mysql, hostname 127.0.0.1, database demo_360, username root, password your_password, hostport 3306, charset utf8mb4, prefix tp_, ];type声明数据库类型TP5底层通过PDO连接不需要额外安装驱动。hostname填本机时使用127.0.0.1如果数据库和Web不在同一台机器则填数据库服务器的内网IP。database必须和你第一步创建的库名一致。username和password对应MySQL账号线上环境不要使用root建议单独创建账号并只授予demo_360库的权限。charset使用utf8mb4因为商品名称、客户备注里可能出现emoji或特殊符号utf8mb4才能完整存储。prefix是表前缀需要和SQL文件里实际使用的前缀保持一致。如果导入时表名就是tp_goods这样的形式这里就是tp_如果SQL里是goods不带前缀这里必须留空。改完配置后先不要急着访问页面。在public_html下创建一个临时PHP文件用PDO直接验证连接是否顺畅?php $dsn mysql:host127.0.0.1;dbnamedemo_360;charsetutf8mb4; $pdo new PDO($dsn, root, your_password); $count $pdo-query(SELECT COUNT(*) FROM goods)-fetchColumn(); echo 商品总数: . $count . PHP_EOL;如果这个文件能正常输出商品总数说明账号权限、数据库名、字符集都没有问题。如果输出的是SQLSTATE错误重点检查密码前后是否有空格以及database是否真的存在。测试完要立刻删除这个临时文件避免暴露数据库信息。3.3 设置Web入口为public_html很多部署事故都源于把项目根目录直接作为Web根目录导致application目录可以被浏览器访问。源码专门提供public_html作为入口目录里面只有index.php和静态资源。Nginx环境下server块应该这样配置server { listen 80; server_name your-domain.com; root /www/wwwroot/project/public_html; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }root直接指向public_html这样浏览器只能访问到入口文件和静态资源。location /里的rewrite把不存在的地址转给index.php?s$1TP5的路由参数通过s接收因此/admin/这类URL才能正确映射到后台模块。如果是Apache需要在public_html下放置.htaccess文件内容通常由安装包自带没有的话可以手动创建作用同样是重写到index.php。3.4 验证后台与前台登录配置完成后访问http://your-domain.com/admin/系统会跳转后台登录页。使用默认账号admin、密码admin888登录。登录成功后左侧导航能看到商品管理、分类管理、库存管理、订单管理和客户管理入口说明数据库连接和目录指向都正常。接着访问/member/这是前台会员登录页。注意前台的账号不是用admin登需要在后台的客户或会员管理里手动创建一个用户。前台和管理员分开会话这一设计既避免客户误入后台数据也方便后期为不同角色配置独立权限。上线前必须做两件事第一把admin888这个默认密码改掉同时去后台打开验证码功能源码中只要服务器安装了GD库验证码开关就会生效第二手动访问.sql、.php等敏感文件路径确认会被入口文件拦截不会直接下载或显示源代码。4. 核心业务实现从商品、库存流水到订单联动4.1 商品管理与分类的控制器逻辑后台商品列表的控制器代码是整个系统的骨架之一它展示了TP5的查询构造器如何组合搜索条件。典型实现如下namespace app\admin\controller; use app\common\model\Goods; use think\Request; class GoodsController extends BaseController { public function index(Request $request) { $keyword $request-get(keyword, ); $categoryId $request-get(category_id, 0); $pageSize $request-get(page_size, 15); $query Goods::where(status, 1); if ($keyword ! ) { $query-whereLike(goods_name, %{$keyword}%); } if ($categoryId 0) { $query-where(category_id, $categoryId); } $list $query-paginate($pageSize); return view(index, [list $list]); } }$request-get(keyword, )从URL参数中读取搜索词第二个参数是默认空字符串保证用户第一次访问时不会报变量不存在的错误。whereLike执行的是模糊查询TP5的查询构造器会过滤变量中的特殊字符但如果你在模型里直接拼接SQL仍然需要对keyword做转义。paginate是TP5自带的分页方法返回的分页对象在模板渲染后就是完整的页码导航。商品分类使用parent_id表示父子关系顶级分类的parent_id为0。读取分类时一般递归查询把分类层级拼成“/”隔开的完整路径这样可以避免在前台展示商品时出现分类名称歧义。点击分类时往商品列表传递category_id后台查询时用where(category_id, $categoryId)精确过滤。4.2 库存变动为什么不能直接加减商品表进销存最容易出错的地方是库存更新。直接在goods表执行stock stock - 1在单用户测试时没问题一旦出现订单取消、退货、手工盘点库存数会变得无法解释。更好的做法是把“改变库存”这个动作封装成独立服务所有业务入口都必须调用它来更新库存和流水表。下面是一个典型的库存变动服务类namespace app\common\service; use app\common\model\Goods; use app\common\model\StockLog; class StockService { public static function change($goodsId, $type, $quantity, $operator) { $goods Goods::find($goodsId); if (!$goods) { throw new \think\exception\ValidateException(商品不存在); } $before (int)$goods-stock; if ($type 2 $before abs($quantity)) { throw new \think\exception\ValidateException(库存不足); } $after $before (int)$quantity; Goods::where(goods_id, $goodsId)-update([stock $after]); StockLog::insert([ goods_id $goodsId, type $type, quantity $quantity, before_stock $before, after_stock $after, operator $operator, create_time time(), ]); } }change方法先读取当前商品库存到$before。$type为2时表示出库这时要判断库存是否足够abs($quantity)把出库数量转成正数再比较避免传入负数导致条件失效。$after的计算结果是当前库存加上传入的quantity出库时调用方传入负数采购时传入正数。写入stock_log时记录变动前后的库存值这是排查库存异常的关键任何时候只要发现某商品账面数与实际不符都能按create_time倒序查看是哪一笔操作造成的。实际项目里所有涉及库存变动的入口包括后台手工调拨、前台订单生成、退货入库都必须调用StockService::change不允许业务代码直接更新goods.stock字段。4.3 订单生成先扣库存还是先写订单订单模块是整个系统业务流的高潮。一张订单生成时需要同时完成三件事写入订单主表、写入订单商品明细、扣减库存并记录流水。顺序设计得不对容易出现“有订单但没扣库存”或“库存扣了但没订单”的中间状态。典型的创建订单方法public function createOrder($userId, $goodsItems) { $orderSn date(YmdHis) . mt_rand(1000, 9999); $totalAmount 0; $orderId Db::name(order)-insertGetId([ order_sn $orderSn, user_id $userId, total_amount 0, status 0, create_time time(), ]); foreach ($goodsItems as $item) { $totalAmount $item[price] * $item[quantity]; Db::name(order_goods)-insert([ order_id $orderId, goods_id $item[goods_id], quantity $item[quantity], price $item[price], ]); StockService::change($item[goods_id], 2, -$item[quantity], user: . $userId); } Db::name(order)-where(order_id, $orderId)-update([ total_amount $totalAmount, ]); return $orderId; }代码里先把订单主表插进去拿到自增的$orderId这样明细表才能关联到正确的订单。然后逐条插入商品明细每插入一条立即调用StockService::change并传入负数数量。这一顺序很关键如果库存不够change会抛出异常订单创建过程被打断数据库事务可以在外层包裹保证已经插入的主表记录回滚避免出现半成品订单。总金额在循环结束后再更新是为了避免循环中途失败时主表金额和明细金额不一致。订单号使用date(YmdHis)加四位随机数同一秒内并发量大时可能冲突生产环境建议再拼接一个随机字符串或使用redis自增序列。客户管理和订单模块之间的联动则通过user_id实现后台在客户筛选时可以通过user_id关联查询订单表计算出该客户的历史累计消费和最近下单时间用于分级管理。5. 二次开发要点与部署期常见坑5.1 用TP5查询构造器完成销售统计进销存系统上线一段时间后最常被要求的功能就是“近30天商品销售排行”。这类统计不需要写复杂的原生SQLTP5的查询构造器支持直接join和聚合$rank Db::name(order_goods) -alias(og) -join(goods g, og.goods_id g.goods_id) -where(og.create_time, between, [strtotime(-30 days), time()]) -field(g.goods_name, SUM(og.quantity) as total_quantity) -group(og.goods_id) -order(total_quantity, desc) -limit(10) -select();join关联goods表是为了在结果里直接显示商品名称避免查出数据后再循环补查商品表。where between使用时间戳数组比在SQL里写DATE()函数能更高效地利用索引。group按商品维度聚合SUM(og.quantity)统计销量order降序排列limit 10取前十个。这样的查询结果直接就是数组模板里遍历即可。5.2 后台权限拦截的基类统一处理后台所有控制器都继承了admin模块的BaseController所以登录校验不需要在每个控制器里重复写。在基类的initialize方法中判断会话即可public function initialize() { parent::initialize(); if (!session(admin_id)) { return $this-redirect(/admin/login/index); } $this-adminId session(admin_id); }parent::initialize()必须放在第一行否则框架的语言包、配置文件和模块数据可能还没有加载完后面读取会话或权限时会出现非预期错误。后台登录成功后写入admin_id前台的member模块则使用member_id作为session键名两者不冲突。如果后续需要更细的角色权限可以在admin_id之外再加role_id在initialize里根据当前请求的控制器名和方法名做节点匹配把不匹配的请求直接跳到无权限提示页。5.3 数据库连接数过高和慢查询排查进销存系统随着单据增多最容易出现的是MySQL连接被打满和慢查询。先用EXPLAIN查看订单明细查询有没有走索引EXPLAIN SELECT * FROM order_goods WHERE order_id 1024;如果type列是ALL说明该查询正在全表扫描需要在order_goods表的order_id字段上建索引。同样地商品列表页如果频繁按分类和库存排序可以建category_id和stock的复合索引。另一个常见问题是在模板循环里查询数据库比如循环商品列表时对每个商品又发起一次客户查询形成N1查询。正确的做法是查询商品列表时把需要的关联字段一次性查出来再用数组映射。上线前把application/config.php里的调试模式关闭把日志级别调整成error否则日常访问都会记录到debug日志几天就能把磁盘写满。部署完成后可以专门用命令行压测一下/admin/和/member/接口的并发响应观察PHP-FPM和MySQL的进程占用再根据瓶颈决定是否启用Redis缓存和页面静态化。本文还有配套的精品资源点击获取
返回列表