从0到1自建轻量级CRM:私有部署、权限设计与数据自主实战指南
做销售管理和客户维护这些年我越来越确定一件事工具不是越贵越好而是越顺手越好。团队最开始也用过各种现成的 SaaS CRM能跑但等要加自定义字段、改权限、拉定制报表的时候要么告诉你要付费要么说“暂不支持”。后来我们干脆自己动手基于团队真实流程做了一套轻量级 CRM取名 DeskcommCRM。Desk 是工位Comm 是沟通合起来就是“在工位边上高效沟通把所有客户关系沉淀下来”。这篇文章不是产品发布会而是想从实操角度把设计思路、技术选型、部署细节和踩过的坑一起讲清楚。不管你是小团队负责人、销售总监还是打算自建系统的开发者应该都能从里面拿到直接能用的东西。1. 为什么会有 DeskcommCRM一次关于“客户关系管理”的重新思考1.1 团队最初的痛免费 CRM 总有那么几个“差一点”很多团队一开始都会跟我一样先去搜“免费 CRM”然后打开注册一个账号把客户名单导进去建好跟进记录模板感觉自己已经数字化了。但用上几周之后问题就一个接一个冒出来。第一个是字段不灵活。销售说“我想加一个‘本次报价有效期’字段”你打开后台配置页翻半天发现免费版根本没有自定义字段这个选项或者只能加三五个文本字段下拉框、日期、金额类型都不全。第二个是数据安全感不足。免费方案的数据基本都存在别人的服务器上连备份都做不到本机留存。你永远不知道明天早上登录时会看到什么改动也不知道“免费套餐”的用户会不会被限制导出甚至某天收到邮件说“产品升级原有数据结构不再支持”。我们当时就吃过一次亏一个现成免费工具突然把“客户标签”功能改到了付费版里标签数据倒还在但已经没法继续编辑整个团队的日常流程直接断了一半。第三个是权限模型太粗糙。免费版基本都是“管理员 普通成员”两档销售主管想看自己组员的客户池没门新来的实习生只想导入数据、打标签结果却能删掉同事的客户。这种权限上的“少了不管、多了失控”对小团队来说比缺功能更难受。1.2 DeskcommCRM 的定位轻量、可控、能协作所以我们做 DeskcommCRM 的时候没有把目标定成“又一个万能 CRM”而是回到三个核心需求统一沉淀所有客户、联系人、跟进记录、待办事项都进系统谁在什么时间做了什么一查就知道。协作有序公私客户分得清楚销售能领用客户主管能看团队进度又不至于互相踩脚。数据自主表结构、字段、权限、报表都可以自己改数据库随时能备份导出业务想怎么迭代就怎么迭代。名字里的 Desk 和 Comm说白了就是“大家在自己工位上把客户沟通这件事做好”。系统做出来之后团队内部也叫它“工位沟通台”比 CRM 这三个字母有画面感多了。1.3 适用边界谁适合用谁别跟风DeskcommCRM 适合 5 到 100 人左右的销售型团队比如做 B 端项目跟进、渠道分销、内容付费、外包服务这类业务核心是“人跟单、单跟人”需要围绕客户历史记录做长期运营。也适合一个人做独立站或者自由职业想有一个自己能掌控的客户台账。但它不适合那些需要复杂 ERP、财务结算、供应链协同的大型集团。那种场景需要的不只是 CRM而是整个企业经营系统DeskcommCRM 的定位从一开始就不是“大而全”。这也是很多自建项目翻车的根因——想在一套系统里解决公司所有问题结果什么都做不好。做系统跟装修房子一样先确认建筑面积和承重墙再考虑刷什么颜色。2. 免费 CRM 与私有部署网站的本质区别别只看价格2.1 数据所有权免费方案的“免费”往往写在看不见的地方这个话题本来应该是常识但我发现很多人在选型时完全没有考虑过。你打开一个免费 CRM 的注册页上面写着“永久免费”你觉得很赚但读一遍隐私条款和服务协议之后会发现你的客户数据、跟进记录包括员工账号信息都存储在对方的数据中心里。一旦平台跑路、政策调整、账号异常你连把数据完整导出来都需要求客服。更现实的是免费产品想要盈利通常会在不显眼的地方做文章免费版只能导出一半字段、导出文件带水印、每个季度强制弹广告页。这些都是看得见的“隐形成本”。我把话说得直白点如果业务数据是你团队最重要的资产之一就不要把资产放在别人随口就能改规则的地方。DeskcommCRM 走的是私有部署路线数据库在我们自己的服务器上每天凌晨自动备份到异地存储。哪怕系统代码要改、表结构要调先把 SQL 导出来一切都安全。碰过几次“数据找不回来”的事之后你一定会认同一个结论数据所有权比任何花哨功能都重要。2.2 可定制性自己的业务逻辑为什么要等别人的开发排期我们刚开始也考虑过一直用现成的免费版然后付费解锁更多字段。但后来发现很多真实业务场景根本无法用标准字段描述。比如“客户是从哪篇文章过来的”这个值既要记录来源渠道又要计算后续转化率标准 CRM 里通常只是一个文本框根本做不了统计。私有部署之后这些都不是问题。想要“来源渠道”下拉框自己在字段配置表里加一条记录就行。想要“报价有效期”在到期前三天自动提醒后端加一个定时任务扫一遍表给负责人发邮件。想要“销售漏斗”按阶段统计直接写 SQL 联表查 dashboard 表再画到前端图表里。我经常跟团队说私有化部署的系统业务需求永远走在功能前面而不是被功能卡住脖子。2.3 “永久在线”不是玄学是工程习惯网上很多人在搜“永久在线的 CRM 网站”我觉得这个词有点误导。任何系统都不敢保证几百年不宕机真正的“永久在线”是指硬件坏了能快速恢复、证书过期不会半夜失效、数据库挂了有自动重启、数据丢了有备份能找回来。自建系统的稳定不靠运气靠三件事进程守护Docker 容器设置restart: unless-stopped服务器重启后服务自动拉起。证书自动化用 Let‘s Encrypt cron 自动续期 HTTPS 证书杜绝“今天页面突然提示不安全”这种事故。备份有规划每天备份数据库异地保留最近 7 天甚至 30 天恢复流程提前演练过。这三步做完普通人感知到的“在线率”已经跟商业 SaaS 差不太多了。至于那种需要在任意网络环境下都能访问的需求请合理选择云服务商和公网域名靠工程手段去保障而不是靠某个工具的“永久免费”承诺。3. 系统架构与技术选型DeskcommCRM 是怎么搭起来的3.1 技术栈的选择与理由真正的实战项目技术选型不是越新越好而是别给后面的自己挖坑。DeskcommCRM 的整套技术栈如下层级选型理由前端Vue 3 Element Plus Vite组件生态完整表格、表单、弹窗这些 CRM 高频交互开箱即用后端Python FastAPI SQLAlchemy开发效率高自带 OpenAPI 文档异步支持好方便后续做接口扩展数据库PostgreSQL JSONB既能保证关系数据的完整性又能在需要自定义字段时用 JSONB 存储不用频繁改表部署Nginx Docker Compose一键编排前端静态文件、后端 API、数据库分开管理故障边界清晰为什么不选 Node.js不是不行而是团队当时 Python 经验更扎实而且 CRM 这种业务后续大概率要写统计脚本、对接邮件、做数据清洗Python 的生态匹配度更高。为什么用 PostgreSQL 而不是 MySQL因为我们需要在“客户表”上支持自定义字段扩展PostgreSQL 的 JSONB 字段可以给不同客户记录存完全不同的扩展信息加一个自定义筛选还能用 GIN 索引加速这在 MySQL 里实现起来要绕不少路。3.2 核心数据模型没有一张表是多余的CRM 的底层说白了是几张核心表。没有上来就塞二十张表而是只保留下面这些基础模型表名关键字段说明usersid, username, password_hash, role_id, department_id, status, invited_by员工账号带软删除和邀请来源记录customersid, name, owner_id, source, stage, tags, phone, email, address, remark, deleted_at客户主表owner_id 决定归属contactsid, customer_id, name, title, phone, email, is_primary联系人一家客户可能有多位联系人follow_recordsid, customer_id, user_id, content, next_step, next_time, created_at跟进记录next_time 用于待办提醒field_configsid, entity, field_key, label, field_type, options, required, sort动态字段配置前端根据它渲染表单operation_logsid, user_id, action, target_type, target_id, detail, created_at核心操作留痕管理层审计用我特别说一下field_configs这张表。它是 DeskcommCRM 能做“自定义字段”的关键。前端的客户表单不是写死的而是启动时先请求一次字段配置然后根据字段类型输入框、下拉框、日期、金额动态渲染。这样销售觉得需要加一个“意向等级”管理员在后台配置界面加一条记录前端下一次刷新就出现了后端存储时会把额外字段写进customers.ext_data的 JSONB 列里。还有operation_logs这个表一开始很多人觉得没必要后来还真救过我们一次。有次两个销售为了同一个客户归属问题吵起来一查日志发现是晚上导入数据时重复创建了同一条客户记录系统自动做了去重合并但归属判断逻辑没覆盖到清清楚楚。以后谁再来说“这客户明明是我的”直接翻日志比吵架有用一百倍。3.3 权限设计让每个角色只看到自己该看的权限模型是自建 CRM 最容易被低估的部分。DeskcommCRM 分成三种维度角色权限RBAC。预设了超级管理员、部门主管、销售、只读访客四类角色。超级管理员管理组织和系统配置部门主管看本部门客户和成员跟进统计销售维护自己的客户和公海客户只读访客只能看报表适合让财务和市场同事进去做数据分析。行级权限数据可见范围。客户记录有一个可见性字段分为私有只有负责人自己可见。团队负责人所在部门全部可见。公开进入公海所有销售可领用。这套逻辑解决了一个特别真实的冲突销售自己开发的客户不想被别人碰但离职交接时“私有客户”又不能带走。我们的做法是私有客户在负责人离职或者超过 30 天未跟随时自动转入公海其他人可领用。按钮权限操作粒度。同一个列表页销售只能编辑自己的客户主管可以转移客户负责人管理员可以删除客户。按钮的显示和隐藏由后端返回的权限码决定前端不做硬编码判断避免“只有路由守卫、接口照样能调”的安全漏洞。4. 核心功能拆解与实操从客户录入到邀请员工全流程4.1 客户录入、去重与批量导入录入是 CRM 的入口入口做不好后面全是脏数据。DeskcommCRM 的新增客户表单不是简单的一堆输入框而是由字段配置动态渲染出来的。必填项只有客户名称和联系电话其他字段按团队需要开启。录入时最核心的是去重校验。我们的规则是手机号和邮箱在插入前会先查索引如果已存在返回“疑似重复客户”的候选人列表用户可以选择合并或者强制新建。这套机制在批量导入时尤为重要。批量导入 Excel 时我强烈建议先下载系统导出的模板不要自己瞎建列。模板里第一行是列名第二行是示例数据第三行才是正式数据不更稳的方式是模板只有表头然后让用户上传后先走预览校验系统把每一行的错误原因列出来比如“手机号格式不对”“邮箱为空但系统设为必填”“客户名称超过50字”用户修正后再确认导入。这里有个实际经验导入时一次尽量控制在 3000 行以内数据量太大不仅要考虑数据库插入时间还要考虑用户社区浏览器的等待耐心。超过 3000 行就拆分批次后台用任务队列异步处理处理完了在页面右上角弹一个通知。4.2 跟进记录和待办提醒过程管理才是 CRM 的灵魂很多团队把 CRM 用成了“通讯录”只顾着录入客户不写跟进记录。这其实是最大的浪费。DeskcommCRM 里进入客户详情页中间主区域就是一张跟时间线。每次电话、微信、见面都可以快速追加一条记录内容包含沟通摘要、客户意向程度、下一步动作、下次跟进时间。跟进步骤不要做得太重。我们的原则是“30 秒能记完”所以跟进记录表单默认只有三个大字段本次沟通内容文本域、客户状态意向、已报价、已成交、已流失、下次跟进时间日期时间选择器。写完了点保存顺便把列表页的“下次跟进”时间给更新了。待办提醒是怎么做的系统每 5 分钟跑一次定时任务扫follow_records.next_time把当前时间超过计划时间的记录查出来给负责人生成一条站内待办如果配置了邮件 SMTP还会发一封提醒邮件。首页仪表盘上默认展示“今日待跟进”和“已逾期跟进”两个区块逾期超过 3 天的记录会标红。这一套流程跑起来之后销售基本不会再漏跟客户因为每天早上打开系统就知道要先给谁打电话。4.3 邀请员工的两种方式与权限初始化总有人问“能不能邀请同事加入系统”我猜之前大家都是用飞鱼、蝉鸣这类产品的时候也踩过添加成员的坑。DeskcommCRM 把“邀请成员”做成了管理员后台的一个独立模块支持两种方式。第一种手动添加账号。适合人少、当场知道所有信息的情况。管理员在“组织管理 → 成员管理”里点“添加成员”填姓名、邮箱/手机号、选择部门和角色然后系统会自动生成初始密码并发送到邮箱。发送成功后对方拿初始密码登录系统会强制要求改密。这个方法的好处是账号立即可用不用等对方看邮件。第二种邀请链接。适合批量邀请、临时加入、新建员工自己设密码的场景。管理员点击“生成邀请链接”系统生成一个带随机 token 的链接比如https://crm.example.com/invite?tokenxxxx可以设置有效期通常 24 小时或者 7 天。把链接发给同事对方打开后自己填姓名、设置密码完成注册后直接进入系统。这里有几个实际坑必须提醒邀请链接 token 建议加过期时间过期后前端提示“链接失效请联系管理员重新生成”不要让他一直有效否则离职员工拿着旧链接回来能重新注册账号。邮件发送失败是最常见的卡点。测试时一定先确认 SMTP 配置没问题我用过一个自建邮件服务端口被云平台封了结果折腾了两小时才发现是 25 端口不通。建议发信用企业邮箱的 SSL 端口比如 465申请一个专用邮箱当发件账号别用个人邮箱。权限初始化一定要跟上。新员工默认加到“普通销售”角色只能看到自己的客户不能删除数据主管角色要有“客户转移”权限方便做离职交接。别偷懒全给管理员后面审计会很痛苦。4.4 仪表盘与数据导出让数据看得见、拿得走仪表盘用的是 ECharts默认展示这几个指标新增客户趋势最近 14 天、跟进次数排行、客户阶段分布、今日待办事项。这些指标的 SQL 都不复杂关键是别一上来就堆十几个图表用户会看麻木。我现在的习惯是只保留“跟动作直接相关”的指标比如销售主管最关系的还是“组员今天跟进了几个客户”“哪些客户 3 天没跟进了”这两个看到位比十个花哨图表都实用。导出功能我踩过一个坑前端生成 CSV 的时候如果直接用 JavaScript 拼字符串用户用 Excel 打开会出现中文乱码。解决方案是在 CSV 文件内容最前面加上 UTF-8 BOM\ufeff这样 Excel 才能正确识别编码。同理导出文件名也尽量用英文加日期比如customers_20250218.csv避免某些浏览器下载时文件名乱码。更高级一点的用法是配置每日汇总邮件。每天早上 9 点系统自动把昨天的新增客户数、跟进次数、即将逾期的客户名单汇总成邮件发给管理员。这样一来不用等大家打开系统管理层就能通过邮件掌握最新业务状态。5. 部署细节如何把一个 CRM 跑成“永久在线”的网站5.1 服务器环境与目录约定先准备一台 2 核 4G 的云服务器系统选 Ubuntu 22.04 LTS 或者 Debian 12 都行。这个配置对 50 人以内的团队完全够用跑 Docker 加 PostgreSQL 和前端静态文件没问题。域名提前解析到服务器 IP建议把crm.example.com这样的子域名专门留给系统用避免和官网混在一起影响维护。我习惯把项目放在/opt/deskcomm下面目录结构如下/opt/deskcomm ├── docker-compose.yml ├── .env ├── backend/ │ ├── app/ │ └── requirements.txt ├── frontend/ │ └── dist/ # 前端构建后的静态文件 └── deploy/ ├── nginx/ │ └── conf.d/ └── scripts/ └── backup.sh.env 文件里放数据库密码、密钥、SMTP 配置这些敏感信息千万别硬编码到 docker-compose.yml 里。5.2 Docker Compose 编排一次启动全部起来这里给一份可直接参考的docker-compose.yml核心片段version: 3.8 services: db: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: deskcomm POSTGRES_USER: deskcomm POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U deskcomm] interval: 10s timeout: 5s retries: 5 backend: build: ./backend restart: unless-stopped env_file: .env depends_on: db: condition: service_healthy volumes: - ./backend/media:/app/media expose: - 8000 frontend: image: nginx:1.26-alpine restart: unless-stopped volumes: - ./frontend/dist:/usr/share/nginx/html:ro - ./deploy/nginx/conf.d:/etc/nginx/conf.d:ro ports: - 80:80 - 443:443 depends_on: - backend volumes: pgdata:几个关键细节一定要说清楚restart: unless-stopped让服务在服务器重启后自动拉起这是“永久在线”的第一道保险。PostgreSQL 的挂载卷pgdata必须有否则容器删除后数据库就没了这是最常见的丢数据事故。depends_on里的condition: service_healthy确保后端启动时数据库已经可用避免连接失败直接崩掉。5.3 Nginx 反向代理与 HTTPS 证书配置Nginx 同时扮演两个角色把/指向前端静态文件把/api反向代理到后端服务。这个配置我踩过好几次坑最典型的是前端路由用了 Vue Router 的 history 模式结果刷新某个子页面时直接 404。配置文件里补上try_files $uri $uri/ /index.html;就解决了。server { listen 80; server_name crm.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name crm.example.com; ssl_certificate /etc/letsencrypt/live/crm.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/crm.example.com/privkey.pem; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }HTTPS 证书我用 Let‘s Encrypt签发命令一行就够certbot certonly --webroot -w /opt/deskcomm/frontend/dist -d crm.example.com然后加一个自动续期任务每天晚上 3 点检查一次证书到期前会自动更新0 3 * * * certbot renew --quiet --deploy-hook docker exec deskcomm-frontend nginx -s reload这一步非常重要。很多人证书到期那天才被发现这不是“永久在线”的合理状态。5.4 数据备份与恢复所有系统工作的前提备份是我最看重的部分。DeskcommCRM 的备份脚本很简单核心就三件事pg_dump导出、保留最近 7 天、上传异地。#!/bin/bash BACKUP_DIR/opt/deskcomm/backups DATE$(date %Y%m%d_%H%M%S) docker exec deskcomm-db pg_dump -U deskcomm deskcomm | gzip $BACKUP_DIR/deskcomm_$DATE.sql.gz find $BACKUP_DIR -name deskcomm_*.sql.gz -mtime 7 -delete # 上传到对象存储或者另一台服务器 rclone copy $BACKUP_DIR/deskcomm_$DATE.sql.gz remote:backup/deskcomm/恢复流程也必须提前演练过。实际恢复的时候一般是这套顺序docker compose down -v # 停止服务并删除数据卷注意会清空当前数据 docker compose up -d db # 先启动新的空数据库 cat deskcomm_20250218.sql.gz | gunzip | docker exec -i deskcomm-db psql -U deskcomm deskcomm docker compose up -d # 启动后端和前端提前演练备份恢复比出事的时候临时百度一百倍有用。我每年都会做两次恢复演练真到了“数据库被误删”那一刻熟练到不用看文档就能恢复。5.5 常见问题排查速查表实操实录现象原因排查与解决方法页面刷新后 404Vue Router history 模式没有 fallback检查 Nginx 是否加了try_files $uri $uri/ /index.html;登录后过一会儿又掉线session/cookie 域名或 Secure 属性配置不对确认前后端域名一致HTTPS 下设置 SameSiteNone 和 Secure邮件邀请发不出去SMTP 端口被防火墙拦截或账号密码错用 SMTP 测试工具先测试改用 465 端口检查日志导出 CSV 中文乱码缺少 UTF-8 BOM文件内容前加\ufeff或者改用 XLSX 格式多人同时编辑同一客户最后保存的覆盖了前面没有做并发控制给 customers 表加 version 字段更新时比较版本号定时备份没执行cron 环境变量里找不到 docker 命令脚本里使用 docker 的完整路径或者在 cron 里加上PATH/usr/bin:/bin6. 我踩过的坑和后续想做的事自建系统这件事最大的坑不是技术而是“什么都想做”。第一版 DeskcommCRM 的时候我一度想塞进合同管理、回款计划、工单系统后来发现光是把客户跟进做好就已经很多事了。我们的原则是每个新功能必须回答“能帮销售省多少时间”这个问题答不上来就不做。另一个经验是权限设计一定要在导入第一批数据之前就想清楚。我们当时觉得 20 个人的团队没必要 phân角色结果一个月后发现销售互相能看到对方的客户报价虽然没出大乱子但团队信任感确实受了影响。后来补了一套“私有 / 团队 / 公海”的可见性规则才把这个问题真正解决。还有一个很实用的小技巧找一个比较细心的销售当“种子用户”新功能先给他一个人内测他走过一遍都没问题再开放给整个团队。这比自己做测试数据高效得多因为他会真的拿着客户信息来操作能暴露很多你坐在工位上想象不到的问题。接下来我准备做两件事。第一给 DeskcommCRM 加企业微信和钉钉的 Webhook 通知这样跟进提醒和待办信息能直接推到聊天软件里。第二把客户导入做成更智能的“智能匹配列”比如用户上传的 Excel 里叫“公司”的列自动对应系统里的客户名称字段减少手动映射的时间。如果你也在折腾自建 CRM 或者类似业务系统这套思路完全可以照着搭只要记住一句话系统是拿来用的不是拿来秀的。