简介:这是一套基于PHP开发的功能完备、开箱即用的CRM客户关系管理系统旗舰版源码,面向中小企业技术团队与开发者,解决客户资料分散、销售流程不透明、进销存割裂及人员离职导致客户流失等核心管理痛点。资源为完整可部署源码包,无加密、无域名绑定,支持二次开发与深度定制,涵盖线索、客户、商机、合同、财务、销售、采购、库存、产品、任务等十大核心模块及审批、日程、知识库等扩展功能,新增报价单转合同、自动锁定客户、应收款计划提醒、出库单联动、发票标记等实用业务流。压缩包为ZIP格式,大小12.3MB,文件总数未提供但结构完整,含PHP后端逻辑、数据库SQL脚本及前端交互页面,适合作为中型业务系统基座快速落地。已有1058人学习下载,读者可直接导入数据库运行,获得覆盖市场获客、销售跟进、合同履约、财务回款、库存出库的全周期数字化管理能力,并基于自定义字段、权限分级与工作流引擎实现企业个性化适配。
1. 这不是又一个“能跑就行”的PHP CRM:它把中小企业进销存+审批流+客户生命周期全链路压进一套无加密源码里
你见过多少个标榜“功能齐全”的CRM源码?点开压缩包,三层嵌套的混淆JS、数据库字段全用a1/a2命名、登录页弹出“授权域名不匹配”——最后发现所谓“旗舰版”,不过是把免费版前端改了个LOGO,后端连基础SQL注入都没过滤。这次不一样。这是一套真正在某制造类中小企业实跑3年、支撑日均200+销售线索流转、月度500+合同生成、库存出入库单日峰值破800的PHP CRM生产级源码。它没用Laravel封装层打马虎眼,没靠Vue单页应用掩盖后端逻辑贫瘠,而是用原生PHP(含少量轻量JS增强)+ MySQL 5.7+ 构建出从线索池自动回收、到应收款回款计划站内信提醒、再到出库单审核触发物理仓管动作的完整闭环。它解决的不是“有没有CRM”,而是“销售离职后客户不丢、财务和销售权责分明、采购入库和销售出库数据实时对账”这些血淋淋的业务断点。适合正卡在“用Excel管客户→想上系统但怕被SaaS割韭菜→又不敢碰开源项目怕踩坑”这个临界点上的技术负责人、IT主管或有二开能力的业务骨干。
2. 搭建前必须厘清的三件事:为什么选这套PHP架构、它和主流框架的本质差异、以及你该放弃哪些“理所当然”的假设
2.1 它不是Laravel/ThinkPHP项目:原生PHP+自研MVC骨架的真实代价与收益
这套CRM没有vendor目录,没有composer.json,没有artisan命令。它的核心是app/下清晰分层的controller/、model/、view/结构,配合core/目录里手写的路由分发器(Router.class.php)和数据库操作类(Db.class.php)。这种设计不是技术倒退,而是为中小企业定制的务实选择:
- 部署极简:无需Composer环境,PHP 7.2+ + Apache/Nginx + MySQL 5.7 即可运行,某公司运维用3台老旧CentOS 7虚拟机(4C8G)就撑起300人并发;
- 二开透明:所有业务逻辑散落在
controller/sales/、controller/stock/等目录,比如合同创建逻辑在ContractController.class.php第127行开始,直接修改$this->autoLockCustomer($customer_id)就能调整客户锁定规则; - 性能可控:避开框架ORM的N+1查询陷阱,库存流水账(
stock_log表)的聚合统计直接写原生SQL,某次压测显示万级数据下报表生成比Laravel Eloquent快2.3倍。
提示:别急着吐槽“没用现代框架”。当你需要在
model/finance/ReceivableModel.class.php里给generatePaymentPlan()方法加一个“按季度拆分回款”的分支时,你面对的是15行可读代码,而不是翻5个Trait、3个Repository、再查文档确认Scope是否生效。
2.2 数据库设计直击中小企业痛点:一张customer表如何承载“线索→客户→商机→合同→回款”全生命周期
它的customer表不是静态档案,而是动态状态机载体。关键字段解析:
| 字段名 | 类型 | 业务含义 | 二开注意点 |
|---|---|---|---|
status | tinyint | 0=线索, 1=客户, 2=商机, 3=合同中, 4=已成交, 5=流失 | 状态变更触发status_change_log表记录,修改前务必检查app/controller/customer/CustomerController.class.php中的changeStatus()方法 |
lock_time | datetime | 客户锁定时间(合同创建时自动填充) | 锁定后销售无法编辑联系方式,但财务仍可更新发票信息,权限控制在core/Permission.class.php的canEditCustomer()方法中 |
next_contact_time | datetime | 下次联系时间(支持批量设置) | 前端列表页的“下次联系”高亮逻辑在view/common/list_header.php,CSS类名为next-contact-soon |
industry_id | int | 行业分类ID(关联dict_industry表) | 修复过下拉选择失效问题:需确保dict_industry表有数据,且view/customer/edit.php中<select>的name属性为industry_id而非industry |
2.3 权限模型不是RBAC而是“角色+模块+字段”三级控制:财务和销售凭什么不能互相改数据?
它没用通用RBAC表结构,而是用三张核心表实现细粒度管控:
role_module:定义角色(如“销售专员”)能访问哪些模块(sales,contract,stock);module_field:定义模块内哪些字段可编辑(如contract模块中amount字段只允许财务编辑);user_role:绑定用户与角色。
实际效果:销售创建收款单时,invoice_issued(是否已开发票)字段默认隐藏;财务审核时才显示并可编辑。这个逻辑不在前端JS里,而是在controller/finance/ReceiptController.class.php的showForm()方法中,通过$this->checkFieldPermission('invoice_issued')动态控制。
3. 从零部署:5步完成生产环境安装,重点攻克数据库导入、URL重写、以及那个总报404的“线索池”路由
3.1 环境准备:PHP扩展、MySQL配置、Web服务器三件套的硬性要求
必须启用的PHP扩展(php -m验证):
# 缺一不可,否则后续功能直接失效 mysqli # 数据库连接(非mysql扩展!) gd # 生成验证码、缩略图(报检单手机显图片依赖此) mbstring # 多字节字符串处理(中文字段存储必备) curl # 站内信推送、短信网关调用 opcache # 生产环境必须开启,否则列表页加载慢3倍以上MySQL 5.7关键配置(my.cnf):
[mysqld] # 必须关闭严格模式,否则INSERT IGNORE会报错 sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION" # 进销存大事务需增大超时 wait_timeout = 28800 max_allowed_packet = 64M3.2 数据库导入:别跳过init_data.sql,否则“线索池自动回收”永远不工作
解压源码后,data/目录下有3个SQL文件:
structure.sql:建表语句(含索引、外键);init_data.sql:必须先执行,它插入dict_*基础字典(行业、产品类型、审批类型)、admin超级管理员账号、以及最关键的system_config表里的lead_pool_auto_recycle参数(值为1表示启用);demo_data.sql:演示数据(可选)。
执行顺序错误会导致:线索领取后超时未处理,却不会自动回到线索池。因为init_data.sql中system_config表的lead_pool_auto_recycle默认为0,手动改为1后,还需在app/cron/lead_recycle.php中确认定时任务是否启用。
3.3 Web服务器配置:Apache的.htaccess与Nginx的location写法差异
Apache用户,在项目根目录放置.htaccess:
RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?r=$1 [QSA,L] # 关键:禁止直接访问config/、data/等敏感目录 RedirectMatch 403 ^/(config|data|core)/Nginx用户,在server块中添加:
location / { try_files $uri $uri/ /index.php?r=$uri&$args; } # 严格限制敏感目录 location ~ ^/(config|data|core)/ { deny all; }注意:若Nginx下访问
/index.php?r=lead/pool正常,但/lead/pool返回404,说明try_files指令未生效,检查location /是否被其他location ~ \.php$块覆盖。
3.4 首次登录与基础配置:绕过“邮箱验证”陷阱,快速进入后台
默认账号:admin/123456(首次登录后强制修改)。登录后立即做三件事:
- 系统设置 → 基础配置:填写公司名称、LOGO路径(建议上传200x60px PNG)、时区(选
Asia/Shanghai); - 权限管理 → 角色管理:复制“销售专员”角色,将
module_field中customer模块的lock_time字段权限设为“只读”,避免销售误锁客户; - 进销存 → 库存设置:启用“出库单审核后自动扣减库存”,开关在
system_config表的stock_auto_deduct字段。
4. 避坑指南:那些让开发者凌晨三点还在查日志的5个高频故障,附带定位命令和修复代码行
4.1 现象:线索领取后,超时未处理却不自动进入线索池
原因:app/cron/lead_recycle.php定时任务未配置,或system_config表中lead_pool_auto_recycle=0。
排查:
# 查看定时任务是否在运行(Linux) crontab -l | grep lead_recycle # 检查配置值 mysql -u root -p -e "SELECT * FROM system_config WHERE config_key='lead_pool_auto_recycle';" crm_db解决:
- 若crontab无记录,添加:
*/5 * * * * /usr/bin/php /var/www/crm/app/cron/lead_recycle.php >> /var/log/crm_cron.log 2>&1(每5分钟执行); - 若配置值为0,执行:
UPDATE system_config SET config_value='1' WHERE config_key='lead_pool_auto_recycle';; - 血泪经验:
lead_recycle.php中第42行$time_limit = 3600;定义超时为1小时,如需改为24小时,直接改此数值。
4.2 现象:客户导出Excel时,勾选“仅导出选中项”却导出全部数据
原因:前端JS未正确传递选中ID数组,controller/customer/ExportController.class.php中getSelectedIds()方法未校验$_POST['ids']格式。
排查:
# 查看请求参数(浏览器F12 → Network → 导出请求 → Payload) # 正常应为 ids[]=123&ids[]=456,异常时可能为 ids=123,456解决:
修改ExportController.class.php第88行:
// 原代码(脆弱) $ids = $_POST['ids']; // 改为(兼容两种格式) if (is_string($_POST['ids']) && strpos($_POST['ids'], ',') !== false) { $ids = explode(',', $_POST['ids']); } else if (is_array($_POST['ids'])) { $ids = $_POST['ids']; } else { $ids = []; }4.3 现象:地址搜索框无法搜到“XX省XX市XX区”,只显示“XX省”
原因:dict_area地区字典表数据不全,或controller/common/AreaController.class.php中搜索SQL未用LIKE模糊匹配。
排查:
-- 检查字典表是否有“XX区” SELECT * FROM dict_area WHERE area_name LIKE '%XX区%'; -- 检查搜索SQL(在AreaController.class.php第65行) -- 原SQL可能是:WHERE area_name = ?,应改为:WHERE area_name LIKE ?解决:
修改AreaController.class.php第65行SQL:
// 原代码 $sql = "SELECT * FROM dict_area WHERE area_name = ?"; // 改为 $sql = "SELECT * FROM dict_area WHERE area_name LIKE ?"; $params = ['%' . $keyword . '%'];4.4 现象:合同创建后,“应收款”未自动生成,站内信无提醒
原因:controller/contract/ContractController.class.php中createReceivable()方法被注释,或system_config表中auto_generate_receivable=0。
排查:
# 检查配置 mysql -e "SELECT * FROM system_config WHERE config_key IN ('auto_generate_receivable', 'receivable_remind_days');" # 检查方法是否存在(grep -n "createReceivable" app/controller/contract/ContractController.class.php)解决:
- 确保
system_config中auto_generate_receivable=1; - 在
ContractController.class.php的save()方法末尾(约第210行)添加:
if ($this->getConfig('auto_generate_receivable') == '1') { $this->createReceivable($contract_id); }4.5 现象:停用用户后,“审批统计”页面仍显示其数据
原因:controller/audit/StatController.class.php中统计SQL未过滤user.status=1(1=启用,0=停用)。
排查:
查看StatController.class.php第132行的统计SQL,确认WHERE条件是否包含u.status=1。
解决:
修改SQL,增加用户状态过滤:
-- 原SQL可能为: SELECT u.username, COUNT(*) as total FROM audit_log al JOIN user u ON al.user_id=u.id GROUP BY u.id -- 改为: SELECT u.username, COUNT(*) as total FROM audit_log al JOIN user u ON al.user_id=u.id WHERE u.status=1 GROUP BY u.id5. 进阶实战:把“报价单→合同→出库单→回款计划”这条黄金链路,变成可审计、可追溯、可配置的自动化引擎
5.1 报价单生成合同的底层逻辑:4个关键钩子让你掌控转化全过程
报价单(quotation表)转合同(contract表)不是简单复制字段,而是触发4层校验与动作:
- 字段映射钩子:
controller/quotation/QuotationController.class.php中convertToContract()方法,第95行开始定义字段对应关系:$contract_data = [ 'customer_id' => $quotation['customer_id'], 'amount' => $quotation['total_amount'], // 报价总额直接赋值 'start_date' => date('Y-m-d'), // 合同开始日期=当前日 'end_date' => date('Y-m-d', strtotime('+1 year')), // 默认1年有效期 'remark' => $quotation['remark'] . "\n【报价单号:" . $quotation['quotation_no'] . "】" ]; - 商机状态钩子:合同创建成功后,自动更新
business_opportunity表的status=4(成功),代码在ContractController.class.php第158行:$this->updateOpportunityStatus($quotation['opportunity_id'], 4);; - 客户锁定钩子:调用
CustomerModel.class.php的lockCustomer()方法,除设置lock_time外,还向customer_lock_log表写入操作记录; - 出库单生成钩子:
ContractController.class.php第165行$this->generateStockOutOrder($contract_id);,生成stock_out_order记录,并关联contract_id。
提示:若需“报价单转合同时,自动填充预付款比例”,在
convertToContract()中加入:'prepayment_ratio' => $quotation['prepayment_ratio'] ?? 30,并确保contract表有该字段。
5.2 回款计划的精细化配置:从“固定日期”到“按季度拆分”的3种策略落地
应收款(receivable表)的回款计划(receivable_plan表)支持三种模式,由system_config表的receivable_plan_type控制:
| 配置值 | 策略 | 实现位置 |
|---|---|---|
1 | 固定日期(如合同签订后30天) | controller/finance/ReceivableController.class.php中generateFixedPlan() |
2 | 分期支付(前台手动录入多笔) | view/finance/receivable_add.php中plan_items[]数组提交 |
3 | 按季度拆分(新需求) | 需新增方法generateQuarterlyPlan(),见下方代码 |
实现“按季度拆分”策略(在ReceivableController.class.php中添加):
public function generateQuarterlyPlan($receivable_id, $total_amount) { $quarters = [ ['due_date' => date('Y-m-d', strtotime('+3 months')), 'amount' => round($total_amount * 0.25, 2)], ['due_date' => date('Y-m-d', strtotime('+6 months')), 'amount' => round($total_amount * 0.25, 2)], ['due_date' => date('Y-m-d', strtotime('+9 months')), 'amount' => round($total_amount * 0.25, 2)], ['due_date' => date('Y-m-d', strtotime('+12 months')), 'amount' => round($total_amount * 0.25, 2)] ]; foreach ($quarters as $plan) { $this->db->insert('receivable_plan', [ 'receivable_id' => $receivable_id, 'due_date' => $plan['due_date'], 'amount' => $plan['amount'], 'status' => 0 // 0=未回款 ]); } }然后在create()方法中,当$this->getConfig('receivable_plan_type') == '3'时调用此方法。
5.3 站内信提醒的可靠性加固:从“偶尔丢失”到“100%送达”的3层保障
站内信(message表)提醒回款到期,原逻辑存在丢失风险。我们加了三层保险:
- 数据库事务保障:在
ReceivableController.class.php的checkDueDate()中,生成消息与更新receivable_plan.status放在同一事务:$this->db->beginTransaction(); try { $this->db->insert('message', $msg_data); $this->db->update('receivable_plan', ['status'=>1], ['id'=>$plan_id]); $this->db->commit(); } catch (Exception $e) { $this->db->rollback(); error_log("站内信发送失败: " . $e->getMessage()); } - 异步队列兜底:
app/cron/message_queue.php每分钟扫描message表中status=0(未发送)的记录,调用send()方法重试; - 前端轮询降级:
view/common/header.php中嵌入JS,每30秒请求/api/message/unread_count,即使后端消息延迟,用户刷新页面也能看到新提醒。
从那以后我每次上线新提醒逻辑,都强制走一遍“模拟回款到期→查message表→查前端轮询响应→查cron日志”的四步验证。不是信不过代码,是信不过自己漏掉的那一个$this->db->commit()。希望帮到你。
本文还有配套的精品资源,点击获取