ThinkPHP5进销存源码改造指南:部署、库存事务与CRM扩展
2026/9/16 13:22:29 网站建设 项目流程

简介:基于Thinkphp5.0开发的进销存客户管理系统源码,适合中小企业在线销售场景,整体覆盖商品采购、销售、库存变动及客户信息维护,可帮助企业统一管理订单与客户关系,提升运营效率。压缩包约15.13MB,共包含2000个文件,其中php文件负责后端业务逻辑,html、css、js构成前端展示与交互,png、gif提供界面素材,md、json等则包含说明文档与配置信息,目录层级清晰,便于定位和二次开发。系统采用前后台分离管理,后台默认账号admin/admin888,前台账户可在后台配置;安装时需导入demo_360.sql并修改application/database.php中的数据库连接,将域名指向public_html即可运行。基于Thinkphp5的路由与扩展特性,源码在性能、路由设计和功能扩展方面有较好基础,尤其适合正在学习TP5框架或需要进销存项目模板的开发者参考借鉴。目前已有117人学习下载,其部署思路、目录结构和业务模块组织方式对实际项目有直接参考价值。

1. 为什么还要选 TP5 写的进销存源码来改?

一套 ThinkPHP 5.0 的进销存客户管理系统源码,就看点而言,没什么新东西。但换个角度:它仍然占据着大量中小公司后台系统的存量份额。采购、销售、库存、应收应付、客户档案,这些业务在很长一段时间内不会消失,而 TP5 的结构简单直白,路由、模型、控制器三层拆分是“半自动”的,新人接手快,老手改起来也顺手。与其从零搭一套进销存,不如在现有源码上加需求:加字段、加报表、改审批流,这才是这类源码的真实用途。本文讲的就是这样一条路径:拿到源码后,本地怎么跑通、数据表怎么组织、库存扣减为什么容易写错、以及把进销存往 CRM 方向靠时最值得改的几个点。适合正要接手 TP5 老项目的开发,也适合被要求“在这套系统上加一个客户回访功能”的实施人员。运行环境没变,业务逻辑没变,变的是你能不能快速抓住它的骨架。

2. 把 TP5 进销存源码跑起来:本地部署的最小前置条件

2.1 用 PHP 5.6 或 7.x 一次性把集成环境配好

TP5.0 对运行环境的要求并不高,它标注的版本底线是 PHP 5.4,但真实使用中很多老源码都跑在 PHP 5.6 上。选集成环境时,电脑上同时有 PHP 7.0 和 5.6 两个版本是常态,切换时要留意php.ini里的扩展。以下是在 Windows 上用 phpStudy 搭建的基本步骤:

# 1. 在 phpStudy 中切换 PHP 版本为 5.6 或 7.0 # 2. 确认以下扩展已在 php.ini 中启用 extension=php_curl.dll extension=php_mbstring.dll extension=php_openssl.dll extension=php_pdo_mysql.dll # 3. 创建站点目录,例如 D:\www\tp5_icrm # 4. 将下载的源码压缩包解压到该目录,保证 index.php 在根目录 # 5. 在 phpStudy 中新增站点,域名填 tp5_icrm.local

这段命令对应的步骤并不复杂,但有一个常见问题:下载的源码如果带着版本目录,比如解压后是tp5_icrm-master,直接把内容往根目录扔时会少一层路径,导致入口文件找不到。TP5 的入口文件位于项目根目录,不是public目录,运行时会把请求分发到public/index.php。部署时如果开启了伪静态,Apache 你需要配置重写规则,Nginx 则需要写一条location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } },这条规则在 TP5 官方手册里反复出现,但套到线上环境时经常会因为 Nginx 配置层级写错而失效,比如把规则写进了location ~ \.php$块里,结果所有非 PHP 请求都直接返回 404。

2.2 导入数据库文件时的编码问题

源码包里通常会附带一个.sql文件,可能是tp5_icrm.sqldatabase.sql。用 Navicat 或命令行导入时,编码最容易翻车。用命令行导入的好处是能看到错误输出:

mysql -uroot -p tp5_icrm < D:/www/tp5_icrm/database/tp5_icrm.sql

这里有几个参数值得注意:

  • -uroot:用户名,如果本地 root 设置了密码,要在-p后紧跟密码,无空格。
  • tp5_icrm:必须提前创建好这个空数据库,否则导入会报Unknown database
  • 文件路径中不要带中文,Windows 命令行对中文路径的解析偶尔会出问题。
  • 导入成功后,用SHOW TABLES;确认表数量,一般进销存系统会有 20 到 40 张表。

如果你打开.sql文件看到开头有SET NAMES utf8mb4;,那么导入后要额外检查连接编码。TP5 的数据库配置里默认字符集是 utf8,而 utf8mb4 是兼容 utf8 的超集,数据本身不会出错,但某些字段里如果有 emoji 表情,写入时会被截断。建议把连接字符集也改成 utf8mb4,而不是只在建表时用。

2.3 修改 database.php 让系统真正连上库

源码跑不通的最大原因,90% 是数据库配置没改。TP5.0 的配置写在application/database.php中,老源码还会在public/index.php里做环境判断,但大多数是直接改这个文件:

return [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'tp5_icrm', 'username' => 'root', 'password' => 'root', 'hostport' => '3306', 'charset' => 'utf8mb4', 'prefix' => 'tp_', 'debug' => true, ];

prefix这个值需要和.sql文件里的表前缀一致,比如建表语句里写的是tp_usertp_order,那 prefix 就填tp_。系统里如果有多个数据源,比如把日志表放到另一个库,配置里就会出现'database' => [...]的数组形式,这时前三个键位会失效,要按主从或分库配置重新组织。改完配置后,访问http://tp5_icrm.local,能跳到登录页说明框架本身没坏,再输入管理员账号密码,如果提示密码错误,去数据库执行一条重置 SQL 把密码改成 MD5 加密的值,常见系统里初始密码是123456的 MD5 字符串,即e10adc3949ba59abbe56e057f20f883e

提示:TP5.0 在 PHP 7.2 以上会产生一些count()参数相关的 Deprecated 警告,这不影响使用,但会在登录后页面上显示报错信息。线上环境建议把app_debug关掉,本地调试则保留。

3. 进销存的数据流:库存表、订单表与客户档案的关系

3.1 先看清这些表各管哪一段

进销存的核心数据流可以用一句话概括:采购入库增加库存,销售出库减少库存,客户表管的是“跟谁做生意”,订单表管的是“每一笔生意具体收发了什么”。绝大多数源码里都逃不开这几张表:

数据表核心字段示例一句话职责
tp_productid, name, stock, cost_price, sale_price商品档案与当前库存快照
tp_customerid, name, phone, contact, level客户资料,CRM 模块的主表
tp_supplierid, name, phone, payable供应商档案,跟采购单关联
tp_orderid, order_sn, customer_id, total_amount, status销售订单或采购订单的主表
tp_order_detailid, order_id, product_id, qty, price订单明细,逐行记录商品和数量
tp_stock_logid, product_id, change_type, qty, create_time库存变动流水,谁在什么时间动了库存

这张表里最容易忽略的是tp_stock_log。很多源码的库存字段直接写在tp_product上,但这只是一个冗余值,真正的库存变更记录应该落在流水表里。如果这套源码只有销量统计而没有流水表,那就意味着它把库存直接写死在商品表里,这种设计在小规模场景下能跑,但一旦出现订单作废、退货、盘点差异,你就无法追踪到“库存是因为什么原因变的”,最后只能靠人工核账。接手源码后的第一件事,就是确认有没有这张表,没有就补。

3.2 库存扣减不能直接写 UPDATE,要用事务加行锁

销售出库时,最直观的写法是UPDATE tp_product SET stock = stock - 1 WHERE id = 1,但多用户同时下单时会产生超卖。TP5.0 里正确处理扣库存的姿势是:在事务里先 SELECT 加锁,再判断并更新。以 TP5 的 Db 类为例:

public function createOrder($customerId, $items) { // 开启事务,保证多个商品扣减要么全部成功,要么全部回滚 Db::startTrans(); try { $totalAmount = 0; foreach ($items as $item) { // lock(true) 生成 SELECT ... FOR UPDATE,锁住商品行 $product = Db::name('product') ->where('id', $item['product_id']) ->lock(true) ->find(); if (!$product || $product['stock'] < $item['qty']) { throw new \Exception('商品 [' . $product['name'] . '] 库存不足'); } // 扣减库存,setDec 底层是 stock = stock - qty Db::name('product') ->where('id', $item['product_id']) ->setDec('stock', $item['qty']); // 写流水,比直接改库存多一步,但日后能追溯 Db::name('stock_log')->insert([ 'product_id' => $item['product_id'], 'change_type' => 1, // 1=销售出库,2=采购入库,3=退货回补 'qty' => $item['qty'], 'create_time' => time(), ]); $totalAmount += $product['sale_price'] * $item['qty']; } $orderId = Db::name('order')->insertGetId([ 'order_sn' => 'SO' . date('YmdHis') . mt_rand(1000, 9999), 'customer_id' => $customerId, 'total_amount' => $totalAmount, 'status' => 1, 'create_time' => time(), ]); Db::commit(); return $orderId; } catch (\Exception $e) { Db::rollback(); throw $e; } }

这段代码里有两个参数值得单独说明。lock(true)是在数据库层面加行级排它锁,事务提交后才释放,它解决的是“两个请求同时读到库存 100,同时扣成 99”的问题。setDec是 TP5 提供的自减方法,等价于UPDATE tp_product SET stock = stock - 5 WHERE id = 1,比先查出来再 UPDATE 少一次往返,性能上没有差异,但必须配合前面的行锁一起用。insertGetId返回自增主键,这段代码没有把订单明细插入tp_order_detail,实际业务里还需要循环写入明细行,做法与上面逻辑同理,注意明细表和主表必须在同一个事务里。

3.3 客户管理查询的常见误区和索引取舍

客户表在进销存里往往被当作“发货地址簿”使用,犯的毛病是用LIKE '%关键词%'全模糊搜索客户名称。这张表数据量不大,几千条记录时全表扫描无所谓,但客户管理系统一旦加了跟进记录、联系人、成交金额汇总,这个查询就会拖慢整个客户列表页。常见的做法是区分精确和模糊两种模式:

-- 精确查询:客户编码或手机号完全匹配,走索引 SELECT * FROM tp_customer WHERE phone = '13800138000'; -- 模糊查询:客户名称包含关键字,必要时配合索引 SELECT * FROM tp_customer WHERE name LIKE '深圳%';

注意,%关键词%这种写在头部的模糊匹配无法走普通 B+ 树索引,深圳%这种前缀匹配则可以。所以客户编号、手机号这类字段,应该用精确匹配;客户名称这种只能模糊匹配的字段,如果表特别大,可以加FULLTEXT全文索引或引入 Elasticsearch,但进销存场景的表规模通常到不了这一步。字段上的索引建多了会影响写入性能,客户表的核心索引就两个:phone建唯一索引,name建普通索引。其他字段一律不建,等报表查询卡了再加,这是进销存这类读多写少的系统里最稳妥的取舍。

4. 把进销存改出 CRM 味:登录会话、客户归属与数据权限

4.1 登录后 SESSION 编号和认证判断

进销存客户管理系统源码和纯静态后台不一样,进销存里的每个操作都涉及业务数据变更,登录会话不能只认“登录过”,还必须区分“是哪个用户”。TP5.0 的登录逻辑一般写在application/index/controller/Login.php里,登录成功后用户信息会塞进 session。接手源码时,首先确认 session 里存的是用户主键还是用户名:

// 登录成功的处理 $user = Db::name('user')->where('username', $username)->find(); if (password_verify($password, $user['password'])) { // 只存用户 ID,避免把密码等敏感字段带进 session session('user_id', $user['id']); session('user_name', $user['realname']); // 如果系统有多角色,还要写入角色 ID,方便做权限判断 session('role_id', $user['role_id']); return json(['code' => 1, 'msg' => '登录成功']); }

session('user_id')是读取值的写法,session('user_id', $value)是写入的写法,这两个在 TP5 里容易搞混。如果源码里用 PHP 原生$_SESSION['user_id'],那也是兼容的,但你在后续代码里要统一访问方式,不要混用。多用户场景下,客户列表一定要根据user_id过滤,否则一个业务员能看见所有业务员的客户,这在真正的客户管理系统里是不可接受的,尤其是涉及销售提成的时候。

4.2 用中间件给客户列表加数据权限过滤

TP5.0 的中间件在application/http/middleware.php里注册,但很多老源码并没有用中间件,权限判断直接写在控制器的_initialize()方法里。这两者可以共存,改造成本最低的方式是新增一个中间件类,然后在控制器构造函数里调用。这里写一个客户数据权限的示例:

class CustomerScope { public function handle($request, \Closure $next) { // 从 session 拿当前登录用户 $userId = session('user_id'); if (!$userId) { return redirect('/index/login'); } // 把用户 ID 绑定到请求对象上,控制器里通过 $request->userId 读取 $request->userId = $userId; // 查询当前用户的角色,管理员不过滤,普通业务员只看自己的客户 $roleId = session('role_id'); if ($roleId != 1) { // 将过滤条件存入请求参数,控制器复用 $request->customerFilter = ['user_id' => $userId]; } return $next($request); } }

中间件执行顺序是先于控制器方法的,所以在这里给$request对象动态绑定的属性,在控制器里能直接$request->customerFilter取到。这种做法比在控制器里重复写where('user_id', session('user_id'))要省事得多,新加一个客户跟进功能时,永远不用记得去拼用户条件。如果这套源码没有user_id字段在客户表上,那就得先加字段和后台分配逻辑,这一步躲不开。用中间件还有一个好处:客户管理模块里的所有接口,包括列表、导出、跟进记录录入,都统一经过这道过滤,不存在漏掉某个接口导致数据泄露的问题。

4.3 报表接口的日期参数必须过滤

进销存系统的报表页面很容易被 SQL 注入,原因是开发时会直接接收前端传来的日期范围。TP5.0 的查询构造器已经做了参数绑定,但如果代码里用了whereRaw或者字符串拼接,风险就回来了。接手后统一查一下whereRawquery(execute(这几个关键字,凡是看到直接把$_GET$request->param()拼进去的,一律改成参数绑定:

// 不安全的写法,不要保留 $list = Db::query("SELECT * FROM tp_order WHERE create_time BETWEEN '{$start}' AND '{$end}'"); // 安全的写法 $list = Db::query( "SELECT * FROM tp_order WHERE create_time BETWEEN ? AND ?", [$start, $end] );

这里要注意?占位符在 TP5 原生查询和where条件里的用法不一样,Db::query的绑定参数用?,而查询构造器的where用法是->where('create_time', 'between', [$start, $end])。两者选一种,不要混用。还有,$start$end虽然经过参数绑定不会注入 SQL,但如果没有做合法性校验,2024-13-45这类非法日期会直接让数据库报错,接口返回 500。严谨的写法是在进控制器时就校验格式,PHP 的strtotime($date) === false用来拦截大多数非法输入。

4.4 客户管理模块的字段扩展方向

进销存源码里的 tp_customer 表通常只有姓名、电话、地址、备注,这离 CRM 客户管理系统差得很远。实际项目中,最常被要求加的字段是“客户来源”、“下次跟进时间”、“所属业务员”。改动很小,只需三步:第一步给表加字段,第二步在后台表单页增加输入框,第三步在列表页把新字段展示出来。但有一个容易漏的点:如果系统里有客户导入导出功能,导出 SQL 时通常会指定SELECT *,新字段会自动带上,而导入功能如果写死了字段数组,新字段不会被导入,需要在导入代码里同步增加。接手时花十分钟做一次全字段排查,比上线后补要省事得多。

5. 上线前用 TP5 系统命令做一次完整自检

TP5.0 框架提供了几条命令行的系统命令,常被忽略但很好用。部署到线上环境之前,在项目根目录执行一套固定检查,能提前暴露环境差异问题。

# 1. 查看框架版本与当前 PHP 环境,确认无误 php think version # 2. 清除缓存目录,避免老代码被 opcache 或 runtime 缓存影响 php think clear # 3. 如果源码自带路由缓存,重新生成一次 php think route:cache # 4. 检查数据库配置能否连通,部分版本支持此命令,不支持时跳过 php think optimize:autoload

php think version是验证 TP5 框架是否正常加载的入口命令。如果这一步报错,说明项目路径或 runtime 目录权限有问题。php think clear清空的是runtime/下的缓存、日志和编译文件,线上更新代码后必须执行一次,否则会出现“改了代码但不生效”的情况。route:cache这个命令在 TP5.0 中只在路由规则全部使用注解或配置时才有加速效果,如果源码里大量使用动态路由Route::rule,缓存反而可能造成路由不匹配,建议执行后跑一遍核心流程,出问题就php think route:clear回滚。

还有一个最容易疏忽的点是 PHP 版本差异。本机用 PHP 5.6 开发,线上用 PHP 7.2,老源码基本不会有致命错误,但会积累大量Deprecated警告,这些警告会写入 TP5 的日志文件,占满磁盘。关闭application/config.php里的'app_debug' => false后,大多数警告不再直接显示,但日志里仍会持续记录。线上服务器如果磁盘空间紧张,还要配置日志按天切割或定期清理runtime/log/目录。

最后提一个验证进销存数据完整性的小技巧:对账。上线后手工做一个闭环测试,采购入库 10 件商品,然后销售 3 件,最后看库存是否等于 7,同时查看tp_stock_log里是否生成了两条分别对应入库和出库的流水。这是整个系统最基本的数据闭环,如果这一步都对不上,不要怀疑数据库,回头查代码里有没有跳过流水表直接改库存的分支——那才是账实不符的真正来源。验证通过之后,再把线上订单号和支付金额做一次 SUM 对比,把客户管理里的成交客户数和订单表去重客户数对齐,这两项对上,系统就可以放心交给业务去用了。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询