简介:这是一套基于PHP开发的企业客户关系CRM管理系统源码,面向中小企业、销售团队及有办公协同需求的开发者,用于搭建客户资源维护与销售流程管理平台。系统覆盖公海管理、线索管理、客户管理、业绩订单、系统设置与权限管理六大模块,可实现客户分类跟进、销售线索转化、订单业绩记录及多用户权限控制,模块化设计便于按业务定制扩展。资源包共约2000个文件,压缩后30.86MB,以397个php业务文件为核心,搭配245个js与85个css构建前端交互,192个html页面、360个gif与247个png提供界面素材,另含80个xlsx、35个xls等数据表格及sql、config配置文件,结构完整。目前已有144人学习下载,适合需要快速部署或二次开发CRM系统的读者参考,可借此理解客户关系管理的功能划分与权限体系,节省从零搭建的时间成本。
1. 一套能直接跑起来的 PHP CRM:从客户建档到办公协同的完整闭环
很多中小团队在选客户管理系统时,第一反应是找 SaaS,但真到落地阶段才发现两个硬伤:一是数据不在自己手里,二是想加个「合同审批联动回款计划」这种定制流程,要么加钱要么排队。这套 PHP 客户关系 CRM 管理系统源码,解决的就是这个问题——它把客户建档、商机跟进、合同回款、办公协同这几块做成了一套可私有部署的完整系统,技术栈是 PHP + MySQL,前后端不分离,部署门槛低,一台普通云主机就能跑起来。
它适合谁?一是手里有 PHP 基础、想给公司搭一套内部客户管理工具的开发者;二是接私活需要快速交付一套 CRM 的外包团队;三是想把现有 Excel 客户台账升级成在线协同的中小企业 IT 负责人。整套源码覆盖了企业 CRM 的核心模块,同时把办公协同(公告、任务、日程)也整合进来,不用再单独拼一套 OA。下面我按「这套系统怎么搭起来 → 核心模块怎么改 → 哪些坑必须提前知道」的顺序,把能抄作业的部分全拆开讲。
2. 环境搭建与数据库初始化:把源码跑起来的第一公里
拿到一套 PHP 源码,最怕的就是「文件一堆、不知道从哪下手」。这套 CRM 的目录结构是典型的 PHP 原生项目布局,没有用 Composer 做重度依赖管理,所以部署路径相对清晰。我一般会先在本地用 phpStudy 或者宝塔面板把环境拉起来,确认能跑通再上服务器,避免在线上环境反复试错。
2.1 运行环境的最低配置与版本选择
这套系统对 PHP 版本的要求不算苛刻,但有几个扩展必须开。下面是我实测下来能稳定跑通的配置组合:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| PHP | 7.4 / 8.0 | 7.4 兼容性最好,8.0 需确认无废弃函数报错 |
| MySQL | 5.7 / 8.0 | 5.7 最稳,8.0 注意字符集用 utf8mb4 |
| Web 服务器 | Apache / Nginx | Apache 需开 rewrite,Nginx 需配伪静态 |
| 必需扩展 | mysqli、pdo_mysql、gd、mbstring、json | gd 用于验证码和头像处理 |
| 可选扩展 | zip、curl | 导出 Excel、对接短信接口时用到 |
PHP 版本这块有个血泪经验:如果你用的是 PHP 8.1 以上,部分老代码里的each()函数和动态属性会直接报致命错误。我一般会先把php.ini里的display_errors打开,跑一遍首页和登录页,看有没有红色报错,再决定是降版本还是改代码。
2.2 数据库导入与配置文件修改
环境确认没问题后,下一步就是把数据库跑起来。源码包里一般会带一个.sql文件,导入方式有两种:命令行或者 phpMyAdmin 图形界面。我习惯用命令行,快且不容易断。
# 登录 MySQL 并创建数据库,字符集必须用 utf8mb4 mysql -u root -p -e "CREATE DATABASE crm_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 导入源码包里的 SQL 文件,注意路径换成你实际存放的位置 mysql -u root -p crm_db < /path/to/crm_database.sql # 导入完成后检查表数量,正常在 30 到 50 张之间 mysql -u root -p -e "USE crm_db; SHOW TABLES;" | wc -l导入完成后,找到源码根目录下的数据库配置文件,通常是config/database.php或者application/database.php(取决于用的是原生还是 ThinkPHP 骨架)。需要改的参数就四个:主机地址、数据库名、用户名、密码。
<?php // config/database.php 典型配置结构 return [ 'hostname' => '127.0.0.1', // 数据库地址,本地用 127.0.0.1 'database' => 'crm_db', // 刚才创建的数据库名 'username' => 'root', // 数据库用户名 'password' => 'your_pass', // 数据库密码 'hostport' => '3306', // 端口,默认 3306 'charset' => 'utf8mb4', // 字符集必须和建库时一致 'prefix' => 'crm_', // 表前缀,导入时如果带了前缀这里要对应 ];这里有个容易翻车的点:表前缀。有些源码包导入后表名是crm_user,有些是user,配置文件里的prefix必须和实际表名对得上,否则登录时会报「表不存在」。改完配置后,访问域名根路径,能看到登录页就说明数据库连通了。默认管理员账号一般在 SQL 文件头部注释里,或者源码包的readme里,通常是admin / 123456这类,进去第一件事就是改密码。
2.3 伪静态与目录权限的常见处理
如果用的是 Nginx,直接访问内页可能会出现 404,这是因为 PHP 框架的路由需要伪静态支持。在站点配置里加一段location / { try_files $uri $uri/ /index.php?$query_string; }就能解决。Apache 的话确认.htaccess文件存在且AllowOverride All已开启。
目录权限方面,runtime、uploads、cache这几个目录需要写权限,Linux 下执行chmod -R 755 runtime uploads cache即可。Windows 环境一般不用管,但如果上传附件失败,优先检查uploads目录是否可写。这些步骤做完,一套 CRM 的骨架就算立起来了,接下来才是真正花时间的部分——按业务改模块。
3. 客户管理与商机跟进模块:字段扩展与状态流转改造
系统能登录之后,你会发现默认的客户字段未必符合你的业务。比如做 B2B 的团队需要「客户行业」「企业规模」,做教育的需要「意向课程」「来源渠道」。这一章讲怎么在不破坏原有逻辑的前提下扩展字段,以及商机状态流转怎么改。
3.1 客户表字段扩展与表单联动
客户信息一般存在crm_customer表里,扩展字段分两步:加数据库列、改表单和列表模板。假设要加一个「客户等级」字段,枚举值为 A/B/C。
-- 在客户表新增客户等级字段,默认 C 级 ALTER TABLE crm_customer ADD COLUMN level VARCHAR(2) NOT NULL DEFAULT 'C' COMMENT '客户等级 A/B/C' AFTER customer_name; -- 如果需要在列表页筛选,建议加索引 ALTER TABLE crm_customer ADD INDEX idx_level (level);数据库加完后,找到客户新增/编辑的模板文件,通常在view/customer/add.html这类路径下,在表单里插入一个下拉框。同时在后端的保存逻辑里接收这个字段,ThinkPHP 骨架一般用$data['level'] = input('level');这种写法。列表页的筛选条件也要同步加,否则字段加了但查不了,等于白加。
我一般会建议把常用扩展字段做成「自定义字段」配置表,而不是每次改代码。但这套源码默认没带这个功能,如果扩展字段超过五个,手动改代码的维护成本会很高,这时候可以考虑在crm_customer旁边挂一张crm_customer_ext扩展表,用customer_id关联,把不常用的字段都丢进去。
3.2 商机状态流转与回款计划联动
CRM 的核心价值在于「跟进过程可视化」。这套系统的商机模块默认有「初步接触 → 需求确认 → 方案报价 → 谈判 → 成交/流失」几个状态。实际用的时候,很多团队会卡在「成交之后回款没人管」这个问题上。
改造思路是在商机状态变为「成交」时,自动生成一条回款计划记录。下面是一段典型的联动逻辑:
<?php // 商机状态更新时的钩子逻辑,放在 Business 模型的 updateStatus 方法里 public function updateStatus($id, $status) { $business = $this->find($id); $oldStatus = $business['status']; // 更新商机状态 $this->where('id', $id)->update(['status' => $status, 'update_time' => time()]); // 状态从非成交变为成交时,生成回款计划 if ($oldStatus != 'deal' && $status == 'deal') { Db::name('payment_plan')->insert([ 'business_id' => $id, 'customer_id' => $business['customer_id'], 'plan_amount' => $business['amount'], // 回款金额取商机金额 'plan_date' => date('Y-m-d', strtotime('+30 days')), // 默认30天后回款 'status' => 0, // 0 未回款 'create_time' => time(), ]); } }这段逻辑的关键参数是plan_date,我默认设成 30 天后,实际项目里可以根据合同账期改成 15 天、60 天或者手动填写。plan_amount直接取商机金额,如果存在分期回款,就需要在回款计划表里支持多条记录,按比例拆分。
注意:状态流转的钩子一定要做幂等判断,否则用户反复点「成交」会生成一堆重复的回款计划。上面的
$oldStatus != 'deal'就是干这个的。
3.3 客户查重与合并的实用处理
客户重复录入是 CRM 落地后最常见的数据质量问题。这套系统默认只对客户名称做了唯一性提示,但实际场景里「同一个客户不同销售各建一条」的情况非常普遍。我一般会在客户保存前加一层查重逻辑,按「客户名称 + 手机号」双条件匹配。
<?php // 客户保存前的查重检查 $exists = Db::name('customer') ->where('customer_name', $data['customer_name']) ->whereOr('mobile', $data['mobile']) ->where('id', '<>', $id) // 编辑时排除自身 ->find(); if ($exists) { // 返回提示而不是直接拦截,让销售确认是否继续 return ['code' => 0, 'msg' => '疑似重复客户:' . $exists['customer_name'] . ',负责人:' . $exists['owner_name']]; }这里我选择「提示但不拦截」,因为有些集团客户确实存在多个分公司分别建档的合理场景。如果直接硬拦截,销售会想办法绕过去,反而更乱。查重规则也可以做成配置项,让管理员决定是「仅提示」还是「强制合并」。
4. 办公协同模块:公告、任务与日程的整合方式
很多 CRM 只做客户管理,但实际办公里「客户跟进」和「内部协同」是分不开的。这套源码把公告、任务、日程做进了同一个后台,好处是不用来回切系统,坏处是模块之间的数据联动需要自己理清楚。这一章讲怎么把协同模块用起来,以及权限怎么控。
4.1 任务分配与客户关联的实现
办公协同里最实用的功能是「给某个客户挂一个跟进任务」。默认的任务模块可能只支持指派给人,但加上客户关联后,销售在客户详情页就能看到「这个客户还有哪些待办任务」。
实现方式是在任务表加一个customer_id字段,然后在客户详情页查询关联任务:
-- 任务表增加客户关联字段 ALTER TABLE crm_task ADD COLUMN customer_id INT NOT NULL DEFAULT 0 COMMENT '关联客户ID,0为不关联' AFTER assign_user_id; -- 查询某客户下的待办任务 SELECT t.id, t.title, t.deadline, u.username AS assign_name FROM crm_task t LEFT JOIN crm_user u ON t.assign_user_id = u.id WHERE t.customer_id = 1001 AND t.status = 0 ORDER BY t.deadline ASC;任务状态一般分「待处理 / 进行中 / 已完成 / 已取消」,我建议在客户详情页只展示「待处理」和「进行中」的,已完成的折叠起来,否则任务一多页面会很长。截止日期临近的任务可以用红色标注,这个在前端模板里加个判断就行。
4.2 公告已读未读与日程提醒
公告模块的坑在于「发了没人看」。默认的公告列表只显示标题和时间,没有已读状态。要解决这个问题,需要一张已读记录表:
-- 公告已读记录表 CREATE TABLE crm_notice_read ( id INT AUTO_INCREMENT PRIMARY KEY, notice_id INT NOT NULL COMMENT '公告ID', user_id INT NOT NULL COMMENT '用户ID', read_time INT NOT NULL DEFAULT 0 COMMENT '阅读时间戳', UNIQUE KEY uk_notice_user (notice_id, user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;用户打开公告详情时,往这张表插一条记录(用INSERT IGNORE避免重复)。公告列表页就可以显示「已读 8/15」这样的进度。日程提醒这块,如果不想接短信或邮件,最简单的做法是在登录后的首页弹窗里查「今天到期的日程」,用WHERE remind_date = CURDATE()查出来直接展示。
4.3 角色权限与数据隔离的配置
办公协同模块最容易出问题的地方是权限。默认可能只有「管理员 / 普通员工」两级,但实际团队里会有「销售主管只能看本组数据」「财务只能看回款相关」这类需求。
这套源码的权限控制一般基于角色表crm_role和节点表crm_role_node。配置数据隔离时,核心是在查询里拼一个where条件:
<?php // 根据当前用户角色拼接数据范围条件 $user = session('user'); $where = []; if ($user['role_id'] == 1) { // 管理员看全部,不加条件 } elseif ($user['role_id'] == 2) { // 主管看本部门 $where['dept_id'] = $user['dept_id']; } else { // 普通销售只看自己的 $where['owner_id'] = $user['id']; } $list = Db::name('customer')->where($where)->paginate(20);这段逻辑要放在所有列表查询的公共方法里,不要每个控制器单独写,否则改权限规则时会漏掉。我见过最离谱的翻车是「客户列表做了隔离,但导出 Excel 没做」,结果销售直接把全公司客户导走了。导出、统计、API 接口这几个入口都要走同一套权限判断。
5. 避坑与排查:部署和二次开发中最容易翻车的五个点
这套源码整体结构不算复杂,但 PHP 原生项目该有的坑一个不少。下面五条是我在实际部署和改代码过程中反复遇到的,按「现象 → 原因 → 解决」整理,能帮你省不少排查时间。
第一条:登录后一直跳回登录页。现象是输入账号密码后页面刷新一下又回到登录页,没有任何报错。原因通常是 session 目录不可写,或者session.save_path配置的路径不存在。解决方法是检查php.ini里的session.save_path,Linux 下确保/tmp可写,Windows 下确保配置的临时目录存在。另一个可能是域名和 cookie 作用域不匹配,把config里的cookie_domain留空试试。
第二条:中文乱码,客户名显示成问号。现象是数据库里存进去的中文在页面上显示为???。原因有三个可能:建库时字符集不是utf8mb4、连接配置里的charset写成了utf8、或者表本身的字符集不对。解决方法是逐层排查——先SHOW CREATE DATABASE crm_db;看库字符集,再SHOW CREATE TABLE crm_customer;看表字符集,最后确认配置文件里是utf8mb4。三层都对齐基本就能解决。
第三条:上传附件报「文件类型不允许」。现象是上传 PDF 或 Excel 时提示类型不合法,但明明格式没问题。原因是源码里的白名单只写了jpg/png/gif,办公场景需要的文档格式没加进去。解决方法是找到上传处理类,在$allowExt数组里补上pdf,doc,docx,xls,xlsx,同时确认php.ini里的upload_max_filesize和post_max_size够大,默认 2M 传不了大附件。
第四条:商机金额统计对不上。现象是仪表盘上的「本月成交金额」和商机列表里手动加总的结果不一致。原因通常是统计 SQL 里用了SUM(amount)但没排除「已流失」状态的商机,或者金额字段存的是字符串类型导致计算精度丢失。解决方法是检查统计条件里是否加了status = 'deal',以及amount字段类型是否为DECIMAL(12,2)而不是VARCHAR。
第五条:改了代码不生效,页面还是旧的。现象是明明改了模板文件,刷新页面毫无变化。原因是这套系统可能开了模板缓存,缓存文件在runtime/cache或runtime/temp目录下。解决方法是手动清空runtime目录,或者在后台「系统设置」里点「清除缓存」。开发阶段建议把app_debug设为true,模板改动会实时生效,不用反复清缓存。
6. 二次开发进阶:用钩子机制把定制逻辑从核心代码里剥出来
改这套 CRM 改到后面你会发现,最怕的不是功能复杂,而是「定制逻辑散落在各处」。今天在客户保存里加一段,明天在商机更新里加一段,三个月后自己都忘了改过哪里。我的习惯是尽量用钩子(Hook)或者事件机制,把定制逻辑集中管理。
这套源码虽然没有完整的插件体系,但可以在几个关键位置手动埋钩子。比如在客户保存、商机状态变更、回款计划生成这三个节点,各留一个hook()调用:
<?php // 在公共函数文件里定义一个极简钩子调度器 function hook($name, $params = []) { $hooks = config('hooks'); // 从配置文件读取已注册的钩子 if (isset($hooks[$name])) { foreach ($hooks[$name] as $callback) { call_user_func($callback, $params); } } } // config/hooks.php 里注册定制逻辑 return [ 'customer_save_after' => [ 'app\custom\Hook::syncToErp', // 客户保存后同步到 ERP ], 'business_deal_after' => [ 'app\custom\Hook::createPayment', // 成交后生成回款计划 'app\custom\Hook::notifyFinance', // 通知财务 ], ];这样做的价值在于:核心代码升级时,你的定制逻辑在app\custom目录下,不会被覆盖。我吃过这个亏——有一次直接改了核心控制器,结果换了个版本源码包,所有改动全丢了,只能凭记忆重写。从那以后我每次做二次开发,都强制走一遍「先找钩子点,再写独立类」的流程。
验证钩子是否生效的方法也很简单,在钩子回调里写一行trace('hook fired', 'info');,然后看日志文件有没有输出。如果没输出,检查配置文件路径对不对、钩子名有没有拼错。这套机制不复杂,但能把「改得乱」和「改得稳」区分开。
希望帮到你。
本文还有配套的精品资源,点击获取