PHP CRM系统源码实战:客户管理、商机跟进与办公协同二次开发指南
发布时间:2026/9/26 22:26:33来源:尧图网络
简介这是一套基于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 版本的要求不算苛刻但有几个扩展必须开。下面是我实测下来能稳定跑通的配置组合组件推荐版本说明PHP7.4 / 8.07.4 兼容性最好8.0 需确认无废弃函数报错MySQL5.7 / 8.05.7 最稳8.0 注意字符集用 utf8mb4Web 服务器Apache / NginxApache 需开 rewriteNginx 需配伪静态必需扩展mysqli、pdo_mysql、gd、mbstring、jsongd 用于验证码和头像处理可选扩展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 关联客户ID0为不关联 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;用户打开公告详情时往这张表插一条记录用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_pathLinux 下确保/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);然后看日志文件有没有输出。如果没输出检查配置文件路径对不对、钩子名有没有拼错。这套机制不复杂但能把「改得乱」和「改得稳」区分开。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网