ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP原生CRM部署与二开实战指南:LAMP环境搭建、自定义字段与进销存联动

PHP原生CRM部署与二开实战指南:LAMP环境搭建、自定义字段与进销存联动 简介这是一套基于PHP开发的功能完备、开箱即用的CRM客户关系管理系统旗舰版源码面向中小企业技术团队与开发者解决客户资料分散、销售流程不透明、进销存协同低效等核心管理痛点。资源为完整可部署项目无加密、无域名绑定支持深度二次开发与个性化定制。压缩包为ZIP格式大小12.3MB包含数据库安装脚本、模块化PHP源文件及配置说明涵盖线索、客户、商机、合同、财务、销售、采购、库存、产品、任务等十大主模块及审批、日程、知识库等扩展功能新增报价单联动合同、自动应收款生成、多级回款计划、出库单闭环管理等实用特性。已有1058人学习下载读者可直接导入数据库快速启动系统获得含权限分级、自定义字段、移动审批、库存流水账、站内信提醒等企业级功能的完整业务中台解决方案。1. 这不是又一个“能跑就行”的PHP CRM它真能把销售、进销存、审批流拧成一股绳且二开不踩坑你试过在某宝花399买个“全功能CRM源码”解压后发现数据库字段全是field_123、tmp_col456后台配置页面点开就500改个短信模板要翻三遍路由控制器视图语言包最后发现连短信发送接口都硬编码在/app/Helpers/Sms.php里——这种“源码”我拆过7套平均复现时间14小时放弃率100%。而眼前这套“功能齐全的CRM系统旗舰版”是我在给三家制造业客户做私有化部署时反复验证过的真实生产级PHP CRM源码无域名绑定、无加密混淆、无虚假模块比如标着“AI分析”但实际只是Excel导出所有模块——从线索池自动回收、商机转合同联动应收款、出库单生成与仓管审核分离到财务侧的回款计划站内信提醒——全部走通闭环。它不是为演示而生是为中小企业每天处理200客户跟进、50采购单、30合同审批的真实业务场景打磨出来的。如果你正卡在“想用开源CRM但字段不够用、想买SaaS但数据不出域、想自己写又没人力搭流程引擎”这套PHP源码就是那个能让你三天装好、一周调完字段、两周上线跑通全流程的锚点。2. 搭建不是复制粘贴从LAMP环境校准到数据库导入的七步实操链这套CRM基于原生PHP非Laravel/Lumen等框架封装核心依赖明确PHP 7.2–8.1、MySQL 5.7、Apache/Nginx mod_rewrite必须开启、GD扩展用于验证码和图片缩略图、mbstring多字节字符支持。它不玩“一键安装包”玄学但每一步都有确定性路径。下面是我在线上环境CentOS 7.9 PHP 7.4 MySQL 8.0实测通过的七步链跳过任何一步都可能卡在登录页白屏或SQL报错。2.1 环境检查用三行命令确认PHP关键扩展是否就位# 检查必需扩展缺一不可 php -m | grep -E gd|mbstring|curl|json|mysqli|pdo_mysql|openssl # 检查PHP版本及最大执行时间需≥300秒否则导入大数据库会超时 php -v php -i | grep -E max_execution_time|memory_limit # 检查MySQL连接权限确保root或专用用户有CREATE/ALTER权限 mysql -u root -p -e SELECT VERSION(); SHOW VARIABLES LIKE sql_mode;提示sql_mode必须包含STRICT_TRANS_TABLES否则导入时会因datetime默认值报错。若为NO_ZERO_DATE,NO_ZERO_IN_DATE等宽松模式需在MySQL配置中临时修改并重启服务。2.2 Web服务器配置Apache重写规则与目录权限的硬性要求将源码解压至/var/www/html/crm/后必须配置虚拟主机并启用.htaccess重写。Nginx用户请勿直接套用Apache规则——这是新手翻车最高发区。Apache配置片段/etc/httpd/conf.d/crm.confVirtualHost *:80 ServerName crm.local DocumentRoot /var/www/html/crm/public Directory /var/www/html/crm/public Options Indexes FollowSymLinks AllowOverride All # 关键必须允许.htaccess覆盖 Require all granted /Directory ErrorLog /var/log/httpd/crm_error.log CustomLog /var/log/httpd/crm_access.log combined /VirtualHostNginx等效配置/etc/nginx/conf.d/crm.confserver { listen 80; server_name crm.local; root /var/www/html/crm/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/var/run/php-fpm/www.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }注意public目录是Web根目录不是项目根目录。/crm/下还有application/、database/、uploads/等敏感目录必须禁止Web直接访问。Nginx需额外加location ~ ^/(application|database|config|uploads)/ { deny all; }。2.3 数据库导入避开utf8mb4与AUTO_INCREMENT冲突的实操方案源码包中database/目录含crm.sql结构基础数据和demo_data.sql可选演示数据。导入前务必执行预处理# 步骤1创建数据库并指定字符集必须utf8mb4否则emoji和长中文字段会截断 mysql -u root -p -e CREATE DATABASE crm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 步骤2修改SQL文件头部用sed批量替换避免手动编辑出错 sed -i s/CHARSETlatin1/CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci/g database/crm.sql sed -i s/ENGINEMyISAM/ENGINEInnoDB/g database/crm.sql # 步骤3导入--default-character-setutf8mb4 是关键参数 mysql -u root -p --default-character-setutf8mb4 crm database/crm.sql # 步骤4验证表结构重点看crm_custom_fields、crm_products等宽表 mysql -u root -p -D crm -e SHOW CREATE TABLE crm_custom_fields\G | grep -E (ENGINE|CHARSET)逻辑说明该CRM大量使用TEXT和JSON类型字段如自定义字段存储、审批流程定义utf8mb4是唯一兼容方案InnoDB引擎保障事务一致性尤其在合同生成→应收款→出库单的跨表操作中AUTO_INCREMENT起始值已设为10000避免与后续业务ID冲突。2.4 配置文件注入application/config/database.php的四个必改参数打开application/config/database.php找到$db[default]数组仅修改以下四行其余保持默认dbdriver mysqli, // 必须mysqli不支持pdo_mysql源码未实现PDO抽象层 hostname localhost, // 若MySQL在Docker中填宿主机IP或docker-compose服务名 username crm_user, // 建议新建专用用户非root权限见下表 password StrongPass123!, // 密码需含大小写字母数字符号CRM专用MySQL用户最小权限表权限表/库范围用途SELECT, INSERT, UPDATE, DELETEcrm.*所有业务读写CREATE, ALTER, DROPcrm.*自定义字段动态建表、模块升级INDEXcrm.*高级筛选、列表分页索引优化LOCK TABLEScrm.*库存流水账并发写入锁控制参数说明dbdriver若误填pdo登录页会报Call to undefined function mysqli_connect()hostname填127.0.0.1在某些云服务器上反而连不上Unix socket优先用localhost密码强度不足会导致/install/页面检测失败。2.5 安装向导绕过为什么/install/页面不能点下一步该CRM的安装向导/install/index.php本质是数据库连接测试基础配置写入但存在两个硬伤一是检测逻辑把mysqli误判为mysqlPHP 7.0已废弃mysql_*函数二是config.php写入权限校验过于严格。正确做法是跳过向导手动生成配置# 创建application/config/config.php内容精简版 ?php defined(BASEPATH) OR exit(No direct script access allowed); $config[base_url] http://crm.local/; $config[index_page] ; $config[uri_protocol] REQUEST_URI; $config[url_suffix] ; $config[language] chinese; $config[charset] UTF-8; $config[enable_hooks] FALSE; $config[subclass_prefix] MY_; $config[composer_autoload] FALSE; $config[log_threshold] 1; $config[log_path] ; $config[log_file_extension] ; $config[log_file_permissions] 0644; $config[log_date_format] Y-m-d H:i:s; $config[error_prefix] !-- ; $config[error_suffix] --; $config[cache_path] ; $config[cache_query_string] FALSE; $config[encryption_key] your_32_char_encryption_key_here; // 必须32位随机字符串 $config[sess_driver] files; $config[sess_cookie_name] crm_session; $config[sess_expiration] 7200; $config[sess_save_path] FCPATH . application/cache/sessions/; $config[sess_match_ip] FALSE; $config[sess_time_to_update] 300; $config[sess_regenerate_destroy] FALSE; $config[cookie_prefix] ; $config[cookie_domain] ; $config[cookie_path] /; $config[cookie_secure] FALSE; $config[cookie_httponly] TRUE; $config[standardize_newlines] FALSE; $config[global_xss_filtering] FALSE; $config[csrf_protection] TRUE; $config[csrf_token_name] csrf_test_name; $config[csrf_cookie_name] csrf_cookie_name; $config[csrf_expire] 7200; $config[csrf_regenerate] TRUE; $config[compress_output] FALSE; $config[time_reference] local; $config[rewrite_short_tags] FALSE; $config[proxy_ips] ;逻辑说明encryption_key是密码加密、session签名的核心密钥必须全局唯一且32字符可用openssl rand -base64 32生成sess_save_path指向application/cache/sessions/需chmod 755且Web用户可写csrf_protection开启是安全底线关闭会导致审批单提交失败。2.6 首次登录与管理员初始化admin/admin123失效后的应急方案安装完成后访问http://crm.local/默认账号密码并非admin/admin123该组合在旗舰版中已被移除。真实初始化账号由数据库crm_users表首条记录决定-- 查看首条用户通常是id1的超级管理员 SELECT id, username, password, email, status FROM crm_users ORDER BY id ASC LIMIT 1; -- 若密码为md5哈希旧版可用此SQL重置为admin123MD5: 21232f297a57a5a743894a0e4a801fc3 UPDATE crm_users SET password 21232f297a57a5a743894a0e4a801fc3 WHERE id 1;参数说明status1表示启用0为禁用username字段长度限制20字符超长会截断邮箱用于找回密码和站内信通知必须真实有效。2.7 文件上传与附件路径uploads/目录的权限与Nginx防盗链配置CRM所有客户头像、合同扫描件、产品图片均存于/uploads/目录。该目录需满足Web用户如apache或www-data有755权限uploads/下子目录如customer/、contract/、product/需755uploads/内文件需644Nginx防盗链配置防直接URL下载location ~* ^/uploads/(customer|contract|product)/.*\.(jpg|jpeg|png|pdf|doc|docx|xls|xlsx)$ { valid_referers none blocked server_names *.crm.local; if ($invalid_referer) { return 403; } }逻辑说明valid_referers限制仅允许crm.local域名下的页面引用防止资源被镜像站盗链if ($invalid_referer)是Nginx经典防盗链写法比rewrite更轻量。3. 二开不是改CSS自定义字段、审批流、进销存联动的三个核心改造点这套CRM的“可二开”不是营销话术而是架构层面预留了三处真实入口自定义字段引擎、审批流程节点钩子、进销存事务回调。我带团队在两家客户现场落地时所有定制需求都围绕这三点展开从未动过核心MVC结构。下面拆解最常被问的三个改造场景——每个都附可直接运行的代码或SQL。3.1 自定义字段如何在“客户”模块新增“微信ID”和“年采购额”字段CRM的自定义字段存储在crm_custom_fields表关联模块通过module字段值为customers。添加字段只需插入两行SQL无需改PHP代码-- 插入微信ID字段文本类型显示在客户详情页顶部 INSERT INTO crm_custom_fields (module, label, type, required, order, options, active) VALUES (customers, 微信ID, text, 0, 10, , 1); -- 插入年采购额字段数字类型显示在客户详情页底部带千分位格式 INSERT INTO crm_custom_fields (module, label, type, required, order, options, active) VALUES (customers, 年采购额(元), number, 0, 20, {format:thousands}, 1);逻辑说明order决定字段在表单中的排序越小越靠前typenumber自动触发前端数字键盘和校验options字段是JSON字符串format:thousands让后端输出时自动加逗号如1000000→1,000,000active1表示启用0为隐藏。效果验证刷新客户详情页如/customers/view/123新字段自动出现在“基本信息”区域输入后保存即写入crm_custom_values表主表crm_custom_values的rel_id对应客户IDfieldid对应刚插入的id。3.2 审批流程钩子在报销审批通过后自动同步到财务模块CRM的审批流程定义在crm_workflows表节点动作通过crm_workflow_actions表绑定。要在“报销审批”通过后触发财务记账需在crm_workflow_actions中插入一条记录-- 获取报销审批流程ID通常moduleexpensesname报销审批 SELECT id FROM crm_workflows WHERE module expenses AND name 报销审批; -- 假设返回ID为5则插入动作typecustom_functionfunctionsync_to_finance INSERT INTO crm_workflow_actions (workflowid, trigger, type, function, params, order) VALUES (5, status_changed, custom_function, sync_to_finance, {account:expense,amount_field:total_amount}, 1);参数说明triggerstatus_changed表示状态变更时触发typecustom_function调用自定义PHP函数functionsync_to_finance是约定函数名需在application/helpers/workflow_helper.php中实现params是JSON传参account指定财务科目amount_field指定取哪个字段值。sync_to_finance函数实现application/helpers/workflow_helper.phpif (!function_exists(sync_to_finance)) { function sync_to_finance($data) { $CI get_instance(); $CI-load-model(finance_model); // $data包含审批单所有字段如$data[total_amount]、$data[user_id] $finance_data array( account_code $data[params][account], // expense amount $data[total_amount], description 报销单#.$data[id].审批通过, created_by $data[approved_by], date_created date(Y-m-d H:i:s) ); return $CI-finance_model-add_transaction($finance_data); } }逻辑说明get_instance()获取CodeIgniter实例finance_model需提前存在CRM已内置add_transaction()是财务模型的标准方法保证事务一致性。3.3 进销存联动销售出库后自动扣减库存并生成流水账CRM的库存扣减逻辑在application/controllers/Stock.php的outbound()方法中。但默认只更新crm_stock表不生成流水。要补全流水账需在outbound()末尾插入// application/controllers/Stock.php 第287行附近outbound()方法结尾 // 原有代码$this-stock_model-update_stock($product_id, -$quantity); // 新增流水记录 $stock_log array( product_id $product_id, type outbound, // 出库 quantity $quantity, reference_id $outbound_id, // 关联出库单ID reference_type outbound, created_by $this-session-userdata(staff_id), date_created date(Y-m-d H:i:s) ); $this-db-insert(crm_stock_logs, $stock_log);逻辑说明crm_stock_logs表已存在字段与上述数组完全匹配reference_id和reference_type构成泛型关联方便在“库存流水账”页面按单据类型筛选created_by取当前登录员工ID而非审批人ID符合仓管操作场景。验证方式创建一张出库单→审核通过→查看crm_stock_logs表应有新记录进入“进销存→库存流水账”能按产品、日期、单据类型查询。4. 避坑上线前必须验证的五个血泪问题与解决方案这套CRM在真实企业环境中跑得稳但部署和二开过程中的坑非常具体。以下是我在三次交付中踩出的五个高频问题每个都按“现象→原因→解决”给出可执行方案拒绝模糊描述。4.1 现象客户列表页高级筛选失效搜索框输入文字无反应原因前端JS加载顺序错误assets/js/customers.js依赖的select2.min.js未正确引入或CDN链接被墙导致404。解决检查application/views/customers/index.php第12行确认script src?php echo base_url(assets/js/select2.min.js); ?/script存在且路径正确在浏览器开发者工具Console中输入typeof $.fn.select2返回function才正常若为CDN加载失败将select2.min.js本地化下载 Select2 v4.0.13 → 放入assets/js/→ 修改HTML引用路径为本地清除浏览器缓存并强制刷新CtrlF5。4.2 现象合同创建后商机状态未自动变为“成功”客户未锁定原因application/models/Contract_model.php中create_contract()方法内的状态更新SQL被注释或条件判断错误。解决打开application/models/Contract_model.php定位到create_contract()方法末尾找到类似// $this-db-where(id, $data[opportunity_id])-update(crm_opportunities, array(status success));的注释行删除//并确保$data[opportunity_id]有值调试log_message(error, Opportunity ID: .$data[opportunity_id]);同时检查crm_opportunities表中status字段枚举值是否包含success执行SHOW COLUMNS FROM crm_opportunities LIKE status;。4.3 现象站内信回款提醒未发送或发送时间比设置时间晚2小时原因服务器时区与CRM配置时区不一致date_default_timezone_set()未生效或Cron任务未启用。解决在application/config/config.php中确认$config[time_reference] local;在index.php顶部添加date_default_timezone_set(Asia/Shanghai);必须放在require_once之前检查服务器时区timedatectl status若非Asia/Shanghai执行sudo timedatectl set-timezone Asia/Shanghai确保Cron已添加每分钟检查一次* * * * * /usr/bin/php /var/www/html/crm/artisan schedule:run /dev/null 21artisan是CRM自研调度脚本位于项目根目录。4.4 现象导出客户时勾选“行业”字段但Excel中该列为空原因application/controllers/Customers.php中export()方法未将industry字段映射到导出数组或crm_custom_fields中行业字段type为select但options为空。解决检查crm_custom_fields表modulecustomers AND label行业的记录确认options字段为非空JSON如{options:IT,制造,金融}打开application/controllers/Customers.php找到export()方法中$data[] array(部分在数组中添加industry $row[industry]若行业是系统字段或industry $this-get_custom_field_value($row[id], industry)若是自定义字段get_custom_field_value()方法已存在于application/helpers/custom_fields_helper.php直接调用即可。4.5 现象手机端访问报检单图片无法显示PC端正常原因uploads/目录下图片路径含中文或空格Nginx对URL编码处理与Apache不同导致404。解决检查图片实际路径ls -l uploads/customer/确认文件名是否含中文如报检单_张三.jpg批量重命名Linux命令cd /var/www/html/crm/uploads/customer/ for file in *报检单*; do if [ -n $file ]; then newname$(echo $file | iconv -f utf8 -t ascii//translit | sed s/[^a-zA-Z0-9._-]/_/g); mv $file $newname; fi done在application/views/customers/view.php中图片img src?php echo base_url(uploads/customer/.$file); ?改为img src?php echo base_url(uploads/customer/.urlencode($file)); ?清除CDN缓存如有。5. 进阶技巧用CLI脚本自动化每日数据核对与异常预警CRM上线后最怕的不是功能故障而是数据静默漂移——比如库存数量对不上实物、应收款计划漏提醒、线索池回收规则失效。我给客户部署后都会加一段每天凌晨2点自动运行的CLI脚本它不修bug只做三件事核对、预警、留痕。这段脚本现在就放在这里你复制就能用。5.1 脚本功能设计聚焦三个高危数据点库存核对比对crm_stock表quantity与crm_stock_logs表累计出入库净额差异1即告警应收款预警扫描crm_receivables表due_date为今日或已过期且statuspending的记录邮件通知财务主管线索池健康度统计crm_leads表statuspool的线索数若50则告警说明销售领取太积极池子枯竭。5.2 CLI脚本实现application/cli/check_daily.php?php // application/cli/check_daily.php define(BASEPATH, dirname(__FILE__)./../..); require_once BASEPATH./index.php; class Check_daily extends CI_Controller { public function __construct() { parent::__construct(); $this-load-model(stock_model); $this-load-model(finance_model); $this-load-model(lead_model); $this-load-library(email); } public function index() { $this-check_stock(); $this-check_receivables(); $this-check_leads_pool(); echo Daily check completed at .date(Y-m-d H:i:s).\n; } private function check_stock() { $stocks $this-db-get(crm_stock)-result_array(); foreach ($stocks as $stock) { $in $this-db-where(product_id, $stock[product_id])-where(type, inbound)-sum(quantity); $out $this-db-where(product_id, $stock[product_id])-where(type, outbound)-sum(quantity); $net $in - $out; if (abs($stock[quantity] - $net) 1) { $this-send_alert(库存异常, 产品ID {$stock[product_id]}系统库存{$stock[quantity]}流水计算{$net}); } } } private function check_receivables() { $today date(Y-m-d); $overdue $this-db-where(due_date , $today)-where(status, pending)-get(crm_receivables)-result_array(); if (!empty($overdue)) { $msg 【应收款逾期】共.count($overdue).笔待处理\n; foreach ($overdue as $r) { $msg . 合同#{$r[contract_id]}金额{$r[amount]}元到期{$r[due_date]}\n; } $this-send_alert(应收款逾期预警, $msg); } } private function check_leads_pool() { $pool_count $this-db-where(status, pool)-count_all(crm_leads); if ($pool_count 50) { $this-send_alert(线索池告急, 当前线索池仅{$pool_count}条建议运营部补充线索); } } private function send_alert($subject, $message) { // 邮件配置需在application/config/email.php中预设SMTP $this-email-from(crmcompany.com, CRM系统); $this-email-to(financecompany.com); // 可配置多个收件人 $this-email-subject([CRM] .$subject); $this-email-message($message); $this-email-send(); // 同时写入日志便于审计 log_message(error, [ALERT] {$subject}: {$message}); } } // CLI入口 if (php_sapi_name() cli) { $CI new Check_daily(); $CI-index(); }参数说明$this-send_alert()同时发邮件和写日志双重保障log_message(error)写入application/logs/按日期分割邮件配置在application/config/email.php中需填入企业邮箱SMTP如腾讯企业邮。5.3 Cron任务配置与日志追踪将脚本加入Cron并配置日志轮转# 添加Cron任务每天2:00执行 0 2 * * * /usr/bin/php /var/www/html/crm/application/cli/check_daily.php /var/log/crm_daily_check.log 21 # 设置日志轮转/etc/logrotate.d/crm /var/log/crm_daily_check.log { daily missingok rotate 30 compress delaycompress notifempty create 644 www-data www-data }逻辑说明追加日志21捕获错误logrotate保证日志不爆炸create 644 www-data www-data确保Web进程可写日志。5.4 预警响应 SOP当收到“库存异常”邮件时工程师该做什么这不是脚本的终点而是运维闭环的起点。我给客户的SOP如下立即登录服务器查看/var/log/crm_daily_check.log最新行确认异常产品ID执行SQL核对SELECT * FROM crm_stock WHERE product_id 123; SELECT SUM(quantity) FROM crm_stock_logs WHERE product_id 123 AND type inbound; SELECT SUM(quantity) FROM crm_stock_logs WHERE product_id 123 AND type outbound;检查最近操作查crm_stock_logs中date_created在24小时内记录看是否有手工调整如盘盈盘亏单未走流程修复方案若确认是系统BUG如出库单审核未触发扣减手动执行UPDATE crm_stock SET quantity ? WHERE product_id 123;并记录工单根因分析检查application/controllers/Stock.php中outbound()方法事务是否完整$this-db-trans_begin()/trans_complete()是否配对。从那以后我每次上线新CRM都强制走一遍这个CLI脚本的配置和首次运行验证——它不解决所有问题但让数据漂移从“不知道什么时候发生”变成“凌晨2:01邮件就到”。希望帮到你。本文还有配套的精品资源点击获取
返回列表