做CRM系统的这些年,我越来越确信一个判断:一个CRM好不好用,第一眼看的不是功能列表,而是打开速度和管理员的脑子清不清楚。这个项目叫DeskcommCRM,是一套面向销售团队与小微企业的客户关系管理系统。我把它做成永久在线的Web站,就是要让销售人员在客户楼下掏出手机就能录入跟单记录,让老板在任意一台电脑上打开浏览器就能看到线索转化漏斗,而不是回到办公室补Excel表格。文章里我不会只贴功能清单,我会把我自己踩过的坑、设计时的取舍、部署时的细节一起写出来,适合正在选型CRM的团队,也适合准备自己动手做一套内部工具的开发者参考。
1. 需求与观察:销售团队为什么离不开CRM
1.1 从Excel到CRM,痛点在哪个环节
很多团队一开始管客户都是用Excel,一张表装下所有客户,运气好的分几个Sheet,运气差的就一列一列往下堆。团队两三个人的时候,这套模式勉强能转,一旦超过五个人,问题马上暴露:A销售录入的客户,B销售看不见;老板要统计本周新增线索,得等大家把文件发到群里;离职销售带走一张表,公司客户资产直接蒸发。
我做DeskcommCRM的起点不在“功能要全”,而在“状态要透明”。我见过太多团队买了一套功能复杂的系统,用了一周就吃灰,原因不是系统不好,是销售根本没有动力录入。要让他们持续用,就必须把“记录”变成“顺手的事”,把“查询”变成“比翻Excel快”。所以这个系统在立项的时候,优先级排在前面的是录入体验、跟进状态自动流转、以及老板一眼能看懂的报表,而不是先堆一堆花哨但没人用的模块。
1.2 DeskcommCRM核心功能模块怎么划分
站在实际使用者的角度,我只保留了几块真正高频的模块,任何多余的功能在早期都会变成学习成本。
客户管理是地基,每条客户记录包含基础资料、来源渠道、当前阶段、负责人和最近跟进时间。线索管理负责承接来自官网、转介绍、展会等不同渠道的原始联系人,销售认领后可以转化成正式客户。跟单动态是整个系统价值的集中体现,每次电话、拜访、微信沟通都记录成一条时间线,后续接手的人点开客户详情就能看到完整历史。公海池机制很关键,超过N天未跟进的客户自动掉入公海,其他销售可以捡起重新跟进,这一条规则解决了很多团队客户资源被私藏的问题。最后是统计看板,按销售维度、渠道维度、时间维度看线索转化率与客户分布,老板不用问别人,自己打开就能看到全局。
这套划分思路的核心是“状态驱动”:客户不再是一行休眠数据,而是带着明确阶段和下一步动作的有生命对象。每个字段、每个按钮都在为“让下一次跟进有依据”服务。
1.3 永久在线的含义不只是一个网址
很多团队以前也用过单机版CRM,管理员在自己电脑上装一套,别人要访问就得开着这台电脑,人一出差整个系统就瘫痪。所谓的永久在线,不是简单地把程序放在服务器上,它意味着:
- 全年无休,销售凌晨出差回来想补记录也能打开;
- 多端适配,手机、平板、电脑通过浏览器访问,不需要安装任何客户端;
- 权限集中管控,人员变动在线调整,离职交接不依赖把文件发给谁;
- 数据集中存储,统一备份,不再出现客户信息分散在个人硬盘上的情况。
永久在线的背后是工程问题:进程要常驻、数据库要定期备份、服务器重启后要自动拉起服务、前端要适配各种屏幕尺寸。这些细节会在第3部分详细拆解。
2. 技术选型与设计逻辑:为什么这么搭
2.1 技术栈选择背后的考量
DeskcommCRM的技术栈不是什么新奇玩意儿,恰恰相反,我刻意选择了更稳妥、更成熟、社区资料非常密集的组合,因为系统要承载业务数据,稳定压倒一切。
后端使用Python的FastAPI框架,原因有几个。第一,异步性能足够,CRM这种以增删改查为主的业务系统并发压力并不极端,FastAPI的吞吐量完全够用。第二,PyDantic提供了类型校验和自动API文档,这对团队协作非常有帮助,前端拿到Swagger文档就能直接联调,省去大量沟通成本。第三,Python的生态让后续扩展报表分析、导出Excel、对接企业微信这类功能都方便。
前端使用Vue 3加Element Plus。Vue的响应式开发体验对表单密集型的CRM系统很友好,Element Plus的表格、表单、弹窗、日期选择器这些组件开箱即用,不需要从头造轮子。这里我特别说一句:很多团队做内部系统上来就搞微前端、搞设计系统,真没必要。CRM的核心场景是表单录入和数据展示,组件库的成熟度比炫技重要得多。
数据库选用PostgreSQL,这一点我也是经过对比后确定的。MySQL当然也能做,但PostgreSQL在复杂查询、JSON字段处理、行列统计上的能力更强。CRM系统里典型的按负责人分组统计、按渠道维度筛选线索、按时间段聚合跟进记录,这些SQL在PostgreSQL里写起来干净利落。同时,PostgreSQL的数组类型、全文检索也能为后续扩展搜索功能提供便利。
2.2 数据模型设计的一个实用示例
很多人觉得数据表设计就是建几个字段,真正做过业务系统才发现,表结构的设计深度直接决定了后续代码好不好写。我拿客户表举例,这是DeskcommCRM项目里我改动次数最多的一张表。
CREATE TABLE customers ( id BIGSERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, phone VARCHAR(20), org_id BIGINT NOT NULL REFERENCES organizations(id), owner_id BIGINT REFERENCES users(id), status VARCHAR(20) NOT NULL DEFAULT 'new', source VARCHAR(30), level VARCHAR(10), address TEXT, remark TEXT, last_followed_at TIMESTAMPTZ, next_follow_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_customers_owner ON customers(owner_id); CREATE INDEX idx_customers_status ON customers(status); CREATE INDEX idx_customers_next_follow ON customers(next_follow_at);几个关键点仔细说一下。
org_id字段隔离了租户数据,哪怕系统同时给多个团队使用,数据也不会串。owner_id记录了当前负责人,它和org_id一起构成了查询时的天然分区边界。status字段保存生命周期状态,从new到following到won再到lost,系统里所有的统计报表都围绕这个字段做聚合。最容易被忽视的是next_follow_at字段,我建议所有做CRM系统的人都把这个字段加上。公海规则、到期提醒、待办列表全部依赖它,没有这个字段,系统就得反复扫描全表比对跟进记录,不仅慢,逻辑也容易出错。
字段设计上有一个原则:把高频查询条件索引化。只要是出现在where条件里的字段,就必须评估是否需要索引。业务量小的时候感觉不到,数据量上来之后,一个缺失的索引会导致页面秒级响应变半分钟超时。
2.3 权限设计,老板和销售的边界
CRM系统的权限做不好,后台垮一半。DeskcommCRM的权限模型并不复杂,但边界清晰。
我采用RBAC加数据范围双重控制。角色有超级管理员、管理员、销售主管、销售顾问。超级管理员有系统全部权限,管理员可以配置员工和客户阶段,销售主管能看自己团队的数据,销售顾问只能看到自己名下的客户和公海池里的客户。
数据范围才是CRM权限设计的核心。同一张客户表,不同角色看到的是不同的行。这不是前端隐藏按钮能解决的,必须在后端查询层强制注入数据范围过滤条件。比如销售顾问查询客户时,后端会自动拼上owner_id = current_user.id或者status = 'public'的条件。这个逻辑写在获取当前用户的依赖函数里,所有接口统一调用,不留给业务代码去碰运气。
这里我踩过一个教训:最初版本为了省事,数据范围过滤散落在各个路由函数中,结果总有几个接口漏掉。后来把所有涉及客户列表的查询都收敛到一个统一的筛选函数,才彻底解决数据越权问题。权限设计宁可早期多花点时间,也别上线后收拾漏洞。
3. 实操过程:从零搭一个永久在线的DeskcommCRM
3.1 环境准备与项目初始化
先交代服务器环境。一台2核4G的云主机就能带动一个中小团队的CRM日常使用,操作系统是Ubuntu 22.04,Python 3.10,PostgreSQL 15,Node.js 18。数据库与后端服务部署在同一台机器上,早期团队完全够用,等并发量上来了再把数据库拆到独立机器即可。
项目结构采用前后端分离,目录如下:
deskcomm-crm/ ├── backend/ # FastAPI 应用 │ ├── app/ │ │ ├── api/ # 路由层 │ │ ├── core/ # 配置与安全 │ │ ├── models/ # 数据库模型 │ │ ├── schemas/ # Pydantic模型 │ │ └── services/ # 业务逻辑层 │ └── requirements.txt ├── frontend/ # Vue 3 应用 │ └── src/ └── deploy/ # 部署脚本与配置初始化后端项目时先把依赖装好:
mkdir deskcomm-crm && cd deskcomm-crm python3 -m venv venv source venv/bin/activate pip install fastapi uvicorn sqlalchemy psycopg2-binary pydantic-settings python-jose[cryptography] passlib[bcrypt] python-multipart依赖里每一行都不白装。SQLAlchemy做ORM映射,python-jose处理JWT令牌,passlib做密码哈希,python-multipart用来处理表单上传。装完之后先用最简单的一个健康检查接口把服务跑起来。
3.2 核心接口实现思路
后端接口不追求多,追求路径清晰、语义明确。我下面给一个客户创建接口的例子,这基本上代表了整个系统的编码风格。
from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from app.core.auth import get_current_user from app.core.deps import get_db from app.models.customer import Customer from app.schemas.customer import CustomerCreate, CustomerOut router = APIRouter(prefix="/api/customers", tags=["customers"]) @router.post("", response_model=CustomerOut) def create_customer( data: CustomerCreate, db: Session = Depends(get_db), user = Depends(get_current_user) ): customer = Customer( org_id=user.org_id, owner_id=user.id, name=data.name, phone=data.phone, source=data.source, status="new", next_follow_at=data.next_follow_at ) db.add(customer) db.commit() db.refresh(customer) return customer注意org_id和owner_id都来自当前登录用户,而不是让前端传进来。这样设计是为了防止越权创建。请求经过get_current_user依赖解析出当前用户信息,后续所有涉及数据归属的字段都以这个为准。
客户列表接口要实现筛选和分页,并且强制数据范围过滤:
@router.get("", response_model=List[CustomerOut]) def list_customers( status: Optional[str] = None, keyword: Optional[str] = None, page: int = 1, page_size: int = 20, db: Session = Depends(get_db), user = Depends(get_current_user) ): query = db.query(Customer).filter(Customer.org_id == user.org_id) if user.role == "sales": query = query.filter( or_(Customer.owner_id == user.id, Customer.status == "public") ) elif user.role == "manager": query = query.filter( Customer.owner_id.in_([m.id for m in user.team_members]) ) if status: query = query.filter(Customer.status == status) if keyword: query = query.filter( or_( Customer.name.ilike(f"%{keyword}%"), Customer.phone.ilike(f"%{keyword}%") ) ) total = query.count() items = query.order_by(Customer.updated_at.desc())\ .offset((page - 1) * page_size)\ .limit(page_size)\ .all() return items这个接口看上去简单,但数据范围过滤、关键字模糊搜索、分页逻辑都齐了。关键点在于销售角色判断处,这里会检查当前用户是普通销售还是主管,不同角色执行不同的过滤逻辑,放在统一的位置而不是散落在各个视图里,这是CRM系统不越权的核心保障。
3.3 部署上云,让系统真正7x24小时在线
开发完成只是第一步,部署才是永久在线的关键。我使用Nginx作为流量入口,把来自公网的HTTP请求转发到本机的uvicorn服务进程。这里我只讲几个最容易出问题的环节。
先写Nginx配置:
server { listen 80; server_name crm.example.com; client_max_body_size 20m; location /api/ { proxy_pass http://127.0.0.1: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; } location / { root /var/www/deskcomm-crm/dist; index index.html; try_files $uri $uri/ /index.html; } }这段配置里有两个容易踩坑的地方。第一是client_max_body_size,如果团队上传的文件比较大,不设置这个字段会导致大文件上传直接413报错。第二是try_files指令,Vue单页应用使用的是history路由,需要把所有的前端路由都回退到index.html,否则刷新页面会出现404。
服务进程的守护和开机自启用systemd来管理。创建一个服务文件,保证服务器重启后CRM能自动拉起,不需要人工登录去启动进程。
# /etc/systemd/system/deskcomm-crm.service [Unit] Description=DeskcommCRM Backend Service After=network.target postgresql.service [Service] User=www-data WorkingDirectory=/var/www/deskcomm-crm/backend ExecStart=/var/www/deskcomm-crm/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 Restart=always RestartSec=5 Environment=PYTHONPATH=/var/www/deskcomm-crm/backend [Install] WantedBy=multi-user.targetRestart=always是核心配置。不管是程序崩溃还是服务器重启,systemd都会在5秒后自动拉起服务。这一步做到了,才算真正意义上“永久在线”的雏形。
前端部署流程也顺手写一下,避免遗漏。生产环境构建后把dist目录上传到/var/www/deskcomm-crm/dist,再通过后端接口实现登录取令牌、存取用户信息、跳转路由。HTTPS证书用一般的免费证书就能搞定,配合自动续期脚本,基本不用人工干预。
3.4 数据备份策略,CRM系统的生命线
做CRM数据备份这件事,我吃过一次亏才总结出完整的方案。有一回数据库文件损坏,幸好当时有每天凌晨的全量备份,才找回了90%的数据。从那时起,我把备份策略定为全量加增量,全量每天一次保留近7天,增量每小时一次保留近24小时。
用crontab写定时任务,核心命令就是pg_dump:
0 2 * * * pg_dump -U deskcomm -F c deskcomm_crm > /backup/deskcomm_$(date +\%Y\%m\%d).dump备份文件不能放在同一台服务器上。我习惯的做法是,每天备份完成后用rsync把文件同步到另一台机器或对象存储里。防止服务器硬盘损坏连带备份文件一起丢。恢复流程也提前演练一次,别等到事故出了才发现备份文件无法恢复。
4. 免费CRM、私有部署与DeskcommCRM的真实区别
4.1 免费SaaS CRM背后真正的成本
最近总有人问我,市面上像蝉鸣CRM、飞鱼CRM这种免费产品用着不也挺好,为什么还要自己搭一套?我的回答是:要看团队的阶段和诉求。
免费SaaS CRM解决的是从无到有的问题,厂商把服务器、数据库、运维这些基础设施全部包办,上线速度快,这是它们的优势。但使用免费产品的同时,有几个成本往往被忽略。首先是数据所有权,客户资料存放在对方的服务器上,数据导出是否顺畅、关闭账号后数据能否完整带走,这些都是问题。其次是定制能力,免费版的功能基本是标准的,销售流程不同、管理方式不同,系统不会迁就你,只能你去迁就系统的逻辑。最后是隐性付费点,免费用户多了自然会推增值服务,某些高级报表、API接口、更多存储空间都需要付费解锁。
不是说免费CRM不能用,而是它适合业务模式偏标准、数据不敏感、预算有限的团队。一旦业务跑起来,销售数据成为核心资产,就必须重新考虑系统的可控性。
4.2 自己搭建私有CRM站点,数据在手心里才踏实
“私人网站”这个说法不太准确,更准确的叫法是“私有化部署”。DeskcommCRM采用的就是这个模式。所有数据存放在自己购买的服务器上,客户资料、跟进记录、合同信息都归团队自己支配。系统的逻辑可以进行深度定制,销售阶段改成自己公司的叫法,公海规则改得符合自己的管理习惯,这些需求自己动手就能实现。
私有化部署也有门槛,这也是很多团队犹豫的原因。需要有人懂Linux操作、数据库维护、基础的前端配置,一旦系统出问题,不能拨打客服电话,只能自己查看日志。但随着低代码服务器管理工具的普及,这个门槛在逐渐降低。2G内存的云主机就能跑起来,后续数据库扩容、迁移也都灵活可控。
我的建议是:团队在5人以下、销售流程还很不稳定的阶段,先用表格或者免费SaaS工具跑通流程,是性价比最高的选择。当团队到了二三十人、销售流程已经形成固定打法的时候,数据资产的价值就凸显了,这时候私有化部署一套DeskcommCRM,长期来看反而省钱省心。
4.3 三种方案怎么选,一张表看完
我做一个常年的对比,普通团队可以对照参考。
| 对比维度 | 免费SaaS CRM | 商业付费CRM | 私有化部署CRM |
|---|---|---|---|
| 上线速度 | 极快 | 较快 | 中等,需要几天部署 |
| 数据归属 | 平台方 | 平台方 | 团队完全掌控 |
| 定制能力 | 弱 | 中等 | 完全可定制 |
| 使用成本 | 免费或低付费 | 按年付费 | 服务器费用加维护成本 |
| 技术门槛 | 无 | 较低 | 需要有技术人员或学习意愿 |
| 长期可靠性 | 依赖厂商策略 | 依赖合同 | 自由度最高,依赖自己运维 |
从这张表可以看到,没有绝对的“最好”,只有“最合适”。DeskcommCRM在做的,就是把私有化部署这条路走通、走顺,让技术背景有限的团队也能轻松拥有自己的客户数据资产。
5. 常见问题排查与运维实录
5.1 会话与登录状态到底怎么保持
CRM是业务系统,不能每次刷新都让用户重新登录,会话保持做不好,销售用了想骂人。DeskcommCRM采用JWT加Redis双通道的方式。
用户登录成功后,后端签发一个有效期为12小时的access token,同时后端把token的哈希存一份在Redis,注销时直接删掉Redis记录实现立即失效。前端把所有API请求统一拦截,在请求头里带上token。这里有个细节:前端token存储建议放在内存变量里,不放到localStorage,防止恶意脚本读取到关键凭证。虽然刷新页面会丢内存token,但配合refresh token机制,可以做到无感续期,整体体验还是很顺滑的。
5.2 数据库连接数打满怎么办
业务上线初期,团队不大,大家从来没想过连接数的问题。直到有一次大家同时打开报表页面,数据库连接数瞬间打满,页面全部卡死。当时查下来发现,每个接口在查询时都新建了一个连接,没有走连接池。
解决办法很简单,在SQLAlchemy创建引擎的时候配置连接池参数:
engine = create_engine( DATABASE_URL, pool_size=20, max_overflow=10, pool_pre_ping=True, pool_recycle=3600 )pool_pre_ping这个参数非常推荐加上,它会在每次从连接池取连接之前先测试连接是否存活,避免取到已经被数据库断开的老连接。这套配置调完,连接问题再没出现过。
5.3 权限错乱的一个隐蔽原因
权限这块我前面说了要统一收口,再补一个实战案例。当时系统上线两周,有销售人员反馈说能看到其他同事的客户列表。排查发现,问题出在列表接口的数据范围过滤条件被一个细节漏掉了,get_current_user依赖函数在处理某些请求头时,解析出来的user对象没有role字段,后续代码默认走了管理员的查询分支。
这个问题的根子在于接口依赖了前端传来的一个用户角色字段,而不是信任服务端通过token解析出来的用户信息。修复方式就是彻底删掉对前端传递用户身份字段的信任,所有身份信息都从JWT里取。这算是一个很经典的越权漏洞,也提醒了我:CRM系统的权限口子一定要把死,任何不该前端传的东西都不能传。
5.4 销售在外网环境下打开系统慢怎么办
系统虽然部署在国内云主机上,但销售在外地出差,访问速度还是会有波动。优化策略主要有三个层面。第一是静态资源走CDN,前端构建后的js和css文件体积不小,CDN能大大缩短加载时间。第二是图片和上传文件不存数据库,改用对象存储,访问时走独立域名。第三是接口层面只返回必要的字段,列表接口不要一次查出所有大字段,详情页需要时再单独请求。
这套优化做完,打开系统的速度体感提升非常明显。我建议做任何Web系统都把这些基础优化做在前面,别等用户抱怨了才回头处理。
5.5 公海规则引出的业务争议
公海池这个功能开发起来并不难,难的是规则怎么设置才能既保证积极性又不制造矛盾。我们早期把过期时间设置成3天,结果有些销售干脆就等同事的客户掉公海,然后捡现成的,团队内部闹得很不愉快。
后来把规则改成了阶梯制:普通客户7天未跟进掉公海,重要级别客户15天未跟进掉公海,同时销售主动标记“持续跟进中”的客户可以自动延长保护期。加了这些业务约束之后,系统体验顺畅不少。这也说明,纯工具侧的开发只是基础,真正有价值的CRM系统一定要能把团队的运营规则落到代码里。
6. 我的一点后续扩展思路与经验
DeskcommCRM做到现在,主流程已经非常稳定。我后续最想做的一件事,是把“跟进动态”模块做成自动化的线索温度计。目前系统已经记录了销售每一次跟进动作、每次通话时长、客户每次查看报价单的时间戳,这些数据能提炼出客户热度分,比如一周内有3次互动记录的高意向客户自动标星,超过30天无任何互动的客户自动提示公海预警。发动机是现成的,状态流转逻辑也能撑起来,只需要在报表层多加几张统计视图。
另一个想扩展的方向是企业微信集成。很多销售习惯用企业微信联系客户,如果CRM能把聊天记录自动同步到客户的时间线下面,销售连录入这一步都省了。这一步做起来工程量大一些,但方向我很确定。
最后分享一个成立至今最深的体会:做业务系统,功能做加法容易,做减法很难。研发过程中会有无数人说“这个按钮加上吧”“这个字段加上吧”,但每一个多余的功能都在稀释核心场景的体验。DeskcommCRM能一直保持轻快,靠的就是不断回头审视哪些功能是真的每天在用。如果你也在做自己的客户管理系统,不妨先用纸笔写下一线销售每天真正会打开哪些页面,让系统围着这几张页面转,后面的事都会顺很多。