资讯详情

资讯详情

小团队自建CRM实战:从技术选型到永久在线部署

做客户管理这件事我踩过不少坑。一开始用 Excel 登记客户字段一多就乱后来试过几个在线 CRM功能确实全但数据都在别人服务器上价格还按人头算团队稍微扩大一点账单就让人头疼。折腾了一圈之后我自己动手做了一套适合小团队用的客户管理系统名字就叫 DeskcommCRM。这篇文章不是产品发布会就是把我从需求梳理、技术选型到部署上线的完整过程记录下来包括踩过的坑和后来填上的洞给同样在考虑自建 CRM 的朋友做个参考。DeskcommCRM 是那种典型的自己动手、数据自己掌控的轻量级 CRM它解决的核心问题有三个——客户资料不散落、跟进过程可追踪、团队成员权限清晰。如果你正在犹豫是继续用免费的在线表格凑合还是订阅一套贵得肉疼的 SaaS CRM这篇文章应该能帮你理清思路。下面我会先从项目设计思路讲起然后是技术选型的考虑再到具体功能怎么实现最后是部署上线和常见问题的排查。全程没有藏着掖着的地方代码层面不好展开的部分我也会用配置片段说清楚。1. 项目背景我为什么非要自建一套 CRM1.1 市面上的 CRM 看着很多真选起来却处处碰壁在动手写 DeskcommCRM 之前我陆陆续续试用过六七款 CRM 产品从国际大厂到国内创业团队做的都有。大厂产品功能全到让人眼花缭乱销售漏斗、工单系统、营销自动化、呼叫中心集成什么都有。但问题也出在全上——我团队当时就五个人根本用不了那么多模块后台配置项上百个光是学怎么建字段就花了一个下午。更让我在意的是数据归属。免费版 CRM 通常有人数限制上传附件大小卡得死死的有些甚至连客户导出功能都要付费才能解锁。换句话说我辛苦录入的客户资料被平台保管着想带走还得先交钱。这种被拿捏的感觉非常不舒服尤其是做销售出身的人对客户数据的敏感性是刻在骨子里的。你需要每天打开系统看到属于自己的客户列表、跟进记录和待办事项而不是每隔几个月收到一封免费版即将到期的提醒邮件。1.2 永久在线的真相数据在自己手里才是真的稳热搜词里频繁出现的永久在线的 crm 网站其实反映了很多人的真实心态一个系统能 7x24 小时访问不用担心服务商倒闭、套餐涨价或者突然改版这就是永久在线的朴素愿望。但我做了 DeskcommCRM 之后才明白永久在线不是靠某个服务商承诺的 SLA 实现的而是靠自己的部署架构和备份策略。简单说我的方案是系统本身部署在一台云主机上数据库每天自动备份到本地和另一台对象存储里应用进程挂了会自动拉起数据库崩了可以从备份恢复。这是一套很基础但很稳固的架构逻辑本质上就是把服务可用性这件事从别人手里接回到自己手里。你也可以用家里的一台旧电脑或者小型 NAS 来跑效果类似只要保证网络稳定和定时备份就行。这套逻辑放到任何规模都成立关键不在于机器配置多高而在于你有没有想清楚数据万一没了怎么办这个问题的答案。1.3 明确边界不做什么比做什么更重要自建系统最容易犯的毛病就是需求蔓延。今天觉得该加个报表明天觉得该写个 App后天觉得该接个企业微信。我的做法是先把不做什么写清楚不做复杂的工作流审批不做销售自动化不做内置电话呼叫。DeskcommCRM 的任务就是管好客户-联系人-跟进记录-待办事项这条主线外加一个简单清晰的多成员权限体系。这个边界帮我节省了大量时间。很多 SaaS CRM 产品之所以庞大是因为他们要服务各种规模、各种行业的客户所以每个功能都得做还得做得通用。而自建系统的优势恰恰在于可以砍掉那些看起来很重要但一年用不上一次的功能把界面做到自己团队用着顺手就行。后来我把这套边界总结成一句话CRM 的核心是客户数据 跟进流程其他都是加分项不是必答题。2. 整体设计与技术选型小团队该用什么技术栈2.1 功能模块怎么划分先画清楚使用路径在设计 DeskcommCRM 时我先把使用路径画了一遍业务员登录系统 → 看到今日待办和我的客户 → 点进客户详情 → 查看历史跟进记录 → 新增一条跟进 → 设置下次跟进时间 → 完成。这条路径非常线性基于它我把系统拆成了四个核心模块第一个是线索管理。线索是还没有转化为客户的潜在需求比如在官网留了表单、在展会上拿到的名片。线索模块要记录来源渠道和初步需求描述支持一键转化为客户。第二个是客户管理这是系统的核心承载公司名称、行业分类、联系人、地址、备注等基础信息。第三个是跟进记录每一次电话、拜访、微信沟通都可以记录在客户档案里形成可回溯的沟通历史。第四个是待办和提醒跟进时设置下次联系时间到点后出现在今日待办里。权限设计上我的原则也很简单业务员只能看到自己名下的客户管理者可以看全部。在还不需要复杂层级审批的阶段这套是否本人创建 是否管理员的二维权限模型完全够用。后面就算团队扩张也只要再加一个部门组长角色中间加一层数据可见范围就行基础模型不用推翻重来。2.2 技术栈选型少折腾就是最大的效率技术选型这件事我的经验是用你最熟练的、社区最活跃的、部署最简单的组合不要因为追求新潮去选冷门框架。DeskcommCRM 后端用的是 Python 的 FastAPI前端是 Vue 3 加 Element Plus数据库用的 PostgreSQL部署在 Ubuntu 云主机上用 Docker Compose 编排。为什么选 FastAPI因为它自带 OpenAPI 文档写完接口自动生成可调试的 API 页面联调的时候省了很多事。异步性能对一个小团队的并发量来说是绰绰有余的。Vue 3 加 Element Plus 则是因为组件成熟表格、表单、弹窗这些 CRM 里高频出现的界面元素都有现成的改改样式就能用。PostgreSQL 相比 MySQL对 JSON 字段的支持更友好我在客户资料里加扩展字段时不用频繁改表结构。最关键的是 Docker Compose。以前部署一个应用要装 Python 环境、配数据库、装 Nginx、设置开机自启哪个环节出错都要排查半天。现在一个 docker-compose.yml 文件搞定所有服务的编排云主机上只要装好 Docker一条命令就能把整个系统拉起来。对于小团队来说这种可复现的部署比任何高深的架构设计都实在。2.3 免费 CRM 与私人网站的差别数据边界是分水岭很多人搞不清楚免费的 CRM和自己搭的私人网站到底差在哪。我用一个例子就能说明白。免费 CRM 就像你租了一个商场的储物柜柜子确实免费但钥匙在商场管理员手里他随时可以打开检查、调整规则商场如果倒闭柜子里的东西你只能搬走。私人网站则像在自己家里打了一个保险柜钥匙完全在你手里里面放什么、怎么放、谁能碰自己说了算。这个对比放到实际操作里意味着几件事。免费 CRM 要限制你存储的客户数量要插入广告或品牌信息可能还会拿你的脱敏数据做产品改进。私人网站没有这些问题但代价是你得自己承担服务器费用、安全维护、功能迭代这些运营成本。对于数据敏感度高的行业比如做企业级销售的客户报价单、合同信息都在 CRM 里数据边界这个问题必须认真对待。3. 核心功能实现与实操细节一行一行敲出来的经验3.1 客户模块字段设计是 CRM 的灵魂客户模块看起来只是简单的增删改查但字段设计的好坏直接决定了系统好不好用。我第一批字段是客户名称、客户类型企业/个人、所属行业、联系人姓名、联系电话、邮箱、所在地区、客户来源、当前状态潜在/跟进中/已成交/已流失、备注。这些字段看起来常规但实际使用中很快暴露了问题销售同事希望记录客户大概的预算量级但放到备注里又不好筛选。后来我把预算量级和预计成交时间这两个字段提成了正式字段用下拉框选择而不是自由输入。这一步很关键因为下拉框能保证数据的规范性统计报表的时候不用清洗脏数据。用 PostgreSQL 的 JSONB 字段可以在客户表里加一个extra_data列用来存放那些还没想好要不要做成正式字段的扩展信息后续如果确认某个扩展字段使用频率很高再把它正式提升为独立列就可以了。以下是客户创建接口的核心代码片段我加了注释供参考from pydantic import BaseModel from typing import Optional class CustomerCreate(BaseModel): name: str customer_type: str enterprise industry: Optional[str] None contact_name: Optional[str] None contact_phone: Optional[str] None email: Optional[str] None region: Optional[str] None source: Optional[str] None status: str potential budget_level: Optional[str] None expected_close_date: Optional[str] None extra_data: dict {} app.post(/api/customers) async def create_customer(customer: CustomerCreate, userDepends(get_current_user)): # 默认把当前用户设为负责人 customer_dict customer.dict() customer_dict[owner_id] user.id result await db.execute( customers.insert().values(**customer_dict).returning(customers.c.id) ) customer_id result.scalar_one() return {id: customer_id, message: 客户创建成功}里面比较容易被忽略的一个细节是owner_id的赋值。创建客户时负责人应该自动取当前登录用户的 ID而不是让用户在前端手动选择。这样做既能防止业务员把客户录到别人名下又能简化表单输入。权限控制的核心就在这里查询时强制带上owner_id 当前用户ID的条件除非当前用户是管理员角色。3.2 跟进记录做到有据可查需要解决三个小问题跟进记录模块要解决三个问题记什么、怎么关联、怎么提醒。记什么我定义了一个结构化表单包含跟进方式电话/微信/面谈/邮件、跟进内容、下一步计划、下次跟进时间。跟进内容我用了大文本框不限制格式因为过度结构化的内容输入成本太高销售更习惯用自然语言记录交流情况。关联关系上每条跟进记录必须关联到一个客户 ID同时记录是谁创建的。这个设计保证了日后查任何一个客户详情页都能按时间倒序看到完整的跟进历史。另外一个实用的设计是跟进时客户联系人的功能——其实说白了就是一个联系人下拉框把客户模块下的多个联系人列出来方便记录这次沟通具体是和谁谈的。提醒机制我用了一个很简单的方式每次创建或更新跟进记录时如果填写了下次跟进时间就自动在待办表里生成一条待办关联客户 ID 和跟进记录 ID。系统每天凌晨跑一次定时任务把当前日期等于待办日期的记录推送到首页的今日待办列表。没有用复杂的消息推送因为小团队最有效的提醒就是在你打开系统时第一眼就能看到。定时任务我用的是 APScheduler在 FastAPI 里作为后台任务启动配置非常简单。3.3 员工邀请与权限管理理解邀请码的正确姿势热搜词里飞鱼 crm 怎么邀请员工这类问题说到底就是在问多成员协作的入口应该怎么做。在 DeskcommCRM 里员工邀请我采用了邀请码 邮箱绑定 角色分配三步走的方式。具体流程是这样的管理员在后台点击邀请成员系统生成一个一次性邀请链接同时填写被邀请人的邮箱和角色业务员/管理员。被邀请人点击链接后填写自己的姓名、手机号、登录密码完成账号激活。这个链接默认 48 小时内有效使用一次后立即失效防止被转发滥用。这里有个细节值得注意邀请链接不要直接从系统后台抄给员工因为员工之间传播容易造成链接泄漏。我当时写了一个简单的邮件发送函数用的 SMTP 服务把邀请链接发到员工邮箱。如果你不想配置邮件服务也可以让管理员生成链接后私聊发给员工但要在后台界面上明确标注此链接仅一次有效请勿转发。权限这块我用的是 JWT Token 加中间件校验。登录成功后签发一个带有user_id和role的 Token前端每次请求都在Authorization头带上这个 Token。后端中间件解析 Token然后根据请求路径判断该角色是否有访问权限。所有客户列表、客户详情接口都默认按owner_id过滤数据管理员则可以在请求头加一个X-View-All: true来跳过这个过滤。这种实现方式没有引入复杂的权限框架但对小团队的场景足够可靠排查问题时也很直观。3.4 数据看板与导出让管理者一眼看懂业务状态CRM 不光是给业务员用的管理者也得通过它掌握全局。所以数据看板我做了三个核心指标新增客户数本周、跟进次数本周、成交转化率累计。看板数据是用 SQL 聚合查询算出来的比如新增客户数就是查询created_at在最近 7 天内的记录数量跟进次数同理。成交转化率则用status won的客户数除以总客户数。除了看板数据导出也是高频需求。Excel 导出的功能我用的是 openpyxl 库后端接口接收查询条件生成 xlsx 文件返回给前端下载。这个功能有两点经验值得分享。第一导出一定要做异步处理如果数据量大同步导出会导致接口超时最好是把导出任务丢到后台队列生成文件后给前端一个下载链接。第二导出的数据要和当前用户在界面上看到的数据范围保持一致不能管理员导出全部数据业务员也导出全部数据否则数据安全就会出大漏洞。4. 部署上线与永久在线的落地保障4.1 Docker Compose 编排一条命令拉起整个系统部署环节是我最想强调的部分。第一次部署这个项目时我先在一台新买的云主机上安装好 Docker 和 Docker Compose然后写了一个docker-compose.yml文件。里面的服务有三个backendFastAPI 应用、dbPostgreSQL 数据库、webNginx用来托管前端静态文件并反向代理后端接口。我的一个经验是数据库目录一定要用 Docker Volume 挂载到宿主机这样即使容器删掉重建数据也不会丢。前端构建出来的静态文件打包进 Nginx 镜像后端代码通过挂载目录的方式热更新省去了每次修改代码都要重新构建镜像的麻烦。第一次部署时我在docker-compose.yml里漏写了容器重启策略结果云主机重启后系统没有自动恢复还是后来通过监控报警发现的。所以restart: unless-stopped这个配置一定要加上它能保证 Docker 服务启动时自动拉起所有容器实现真正意义上的永久在线。下面是docker-compose.yml的核心配置片段version: 3.8 services: db: image: postgres:15 container_name: deskcomm-db restart: unless-stopped environment: POSTGRES_USER: deskcomm POSTGRES_PASSWORD: your_strong_password POSTGRES_DB: deskcomm_crm volumes: - db_data:/var/lib/postgresql/data networks: - deskcomm_net backend: build: ./backend container_name: deskcomm-backend restart: unless-stopped depends_on: - db environment: DATABASE_URL: postgresqlasyncpg://deskcomm:your_strong_passworddb:5432/deskcomm_crm SECRET_KEY: change_this_to_a_random_secret volumes: - ./backend:/app - upload_data:/app/uploads networks: - deskcomm_net web: image: nginx:1.25 container_name: deskcomm-web restart: unless-stopped ports: - 80:80 - 443:443 volumes: - ./frontend/dist:/usr/share/nginx/html - ./nginx/conf.d:/etc/nginx/conf.d - ./certs:/etc/nginx/certs - upload_data:/usr/share/nginx/html/uploads:ro depends_on: - backend networks: - deskcomm_net volumes: db_data: upload_data: networks: deskcomm_net:需要注意的细节是上传文件的目录。前后端分离部署时用户上传的图片和附件如果只存在后端容器里Nginx 是无妨直接服务的。我的做法是用一个名为upload_data的共享 Volume 同时挂载到后端和 Nginx 容器后端写入文件Nginx 通过/uploads路径直接读取并返回静态文件这样一个简单的配置就解决了文件服务的问题。4.2 备份策略没有备份的在线都是耍流氓在任何系统里我都坚持一个原则没有备份的在线都是耍流氓。DeskcommCRM 上线第二天我就写了一个备份脚本每天凌晨 3 点执行。备份分成两部分数据库用pg_dump导出为 SQL 文件上传目录用rsync同步到另一个云存储空间。备份保留最近 30 天的记录超过 30 天的自动清理。备份脚本本身不复杂复杂的是验证备份有效。我吃过一次亏备份跑了半年结果有天数据库突然出问题恢复备份时才发现备份文件是空的——因为当时数据库密码改过pg_dump的命令没同步更新。所以现在我的备份脚本最后一步会主动下载最近一个备份文件用pg_restore到一个临时数据库里执行一次能恢复成功才标记为备份有效否则发报警通知。这套自检机制虽然多花了几分钟时间但每次想到数据能恢复心里就踏实很多。4.3 进程守护与监控告警让故障暴露在下一分钟除了 Docker 的重启策略我还单独配了一套监控告警。用 Uptime Kuma 这个开源工具定时检查我的 CRM 域名每 5 分钟发一次 HTTPS 请求如果响应码不是 200就通过 Telegram Bot 发消息到我的手机上。这套方案成本为零效果却很好——有一次云主机网络波动导致服务中断我在家还没醒手机通知已经到了。监控要在两个层面同时做一个是外部视角看网站能不能正常访问一个是内部视角看数据库和服务器的负载情况。Uptime Kuma 负责外部视角内部视角则用node_exporter加 Prometheus 来做不过对小团队而言这个可以晚点再上。最优先要做的是服务挂了能自动拉起 拉起失败有人知道这两个基础能力搞定这两点永久在线的目标已经完成了一大半。5. 常见问题与排查实录那些坑我替你踩过了5.1 多人同时登录Token 互相挤下线系统上线第二周有同事反馈A 在办公室电脑上登录了系统晚上回家又在笔记本上登录结果办公室电脑上的操作突然报未授权错误。排查之后发现是我在签发 JWT Token 时用了同一个SECRET_KEY但没有实现 Token 失效机制新登录的 Token 并不会让旧 Token 失效理论上不该互相挤下线。真正的原因出在前端Axios 拦截器在请求返回 401 时会自动跳转到登录页而前端在本地存储 Token 时key 名写死了多个浏览器标签页共用同一个 localStorage其中一个标签页 Token 过期刷新时把所有的 Token 都清了。解决方案是把 Token 存储从 localStorage 改成 sessionStorage每个标签页独立存储一份互不影响。这个问题的排查花了我大半天时间但说到底就是存储层级选错了经验记下来后面再也没犯过。5.2 上传的附件在列表里看得到但下载时 404还有一个很经典的部署坑上传的图片在前端列表里显示正常点击下载却 404。原因是前端页面里的图片地址是相对路径/uploads/xxx.jpg浏览器实际请求的是 Nginx 服务而 Nginx 的/uploads路径没有正确代理到后端的上传目录。我在排查时先用curl直接访问后端接口发现接口返回 200文件确实存在。然后curl访问 Nginx 的/uploads地址拿到 404。问题定位到 Nginx 配置上。我最初写的 Nginx 配置只做了前端静态文件服务没有加/uploads的alias指向。后来在配置片段里加了下面这行重启 Nginx 后一切正常location /uploads/ { alias /usr/share/nginx/html/uploads/; }这类问题的排查思路其实通用先看后端是否正常再看中间层Nginx是否转发了正确路径最后看前端请求的 URL 是否拼接正确。三层逐一排除比乱改配置高效得多。5.3 邀请链接明明没过期却被提示邀请无效邀请链路有一个隐藏 bug员工在邮件里点击邀请链接时如果邮箱客户端自动解析并访问了一次链接很多邮件服务商为了检测链接安全性会这么做那么后端的used标记会被置为true等员工真正点击时链接已经失效了。排查这个问题的过程中我在邀请链接上增加了一个clicked_times字段第一次被访问时不立即置为无效而是先记录一个时间戳如果来自同一个 IP 的第二次访问才真正标记为已使用。更简单的做法是链接点击后进入一个确认信息页面而不是直接完成注册用户需要手动填写姓名和密码这一步能有效过滤掉邮件预检产生的无效访问。这个小改动上线后邀请链接失效的反馈就再也没出现过。5.4 数据库连接数被打满整个系统响应变慢某天下午系统突然变得很卡接口响应时间从几十毫秒涨到了几秒。查日志发现 PostgreSQL 报too many connections错误。原因是 FastAPI 的异步数据库连接池配置不对默认连接池大小只有 5但连接池的回收机制没设好空闲连接一直占着不释放请求一多就濒临耗尽。解决方法是把create_async_engine里的pool_size调大到 20max_overflow设为 10同时设置pool_pre_pingTrue让连接在每次复用前先做一次有效性检测防止把已经断掉的连接分配给请求。配置调整后的效果立竿见影系统再也没有出现过连接耗尽的问题。这个经验告诉我数据库连接池的参数不是默认就合理的一定要根据实际并发量做调整。6. 写在最后的几点实在话DeskcommCRM 从想法到落地前后花了两三周时间。很多人听到自己开发一套 CRM就觉得工程量大其实拆解下来核心就是客户管理、跟进记录、待办提醒、员工权限、数据看板这些模块每一个都不算复杂难点在于想清楚自己的真实需求并控制住添加功能的冲动。在整个过程中我感触最深的一件事是工具的价值不在于功能数量而在于能否真正贴合使用习惯。SaaS CRM 产品做得再大也不可能比你自己更懂自己的业务。自建一套系统的意义不只是省下订阅费更是把客户数据牢牢攥在自己手里。这种感觉就像从租房子变成了住自己的房子想刷一面墙不用再问房东同不同意。如果你也打算自建 CRM我的建议是先别急着写代码花一两天时间把业务员的真实工作流程走一遍列出那些最痛的点然后只做解决这些痛点的功能。至于技术选型选你最有把握的、社区资料最多的那套组合就行。系统上线之后把备份和监控当作一等公民来对待这两件事做好了剩下的事情都好说。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →