从零自研轻量级CRM系统:架构设计、数据权限与销售漏斗实战
去年年中团队规模还不到三十人客户信息却已经散落得到处都是销售各自Excel表格里的名单、微信聊天记录里的报价与承诺、个人笔记里记录的跟进时间甚至有几家重复联系、差点撞单的尴尬情况。每次管理层想看一眼这个月到底有多少潜在客户、漏斗走到哪一步了都要拉上销售一个个口述统计出来的数字还经常对不上。当时也试过市面上几款成熟的SaaS CRM但要么配置流程太重、上线周期长要么核心数据全部托管在厂商服务器上老板始终不放心。反复权衡之后我决定基于团队真实业务流程自行开发一套轻量级客户关系管理系统——DeskcommCRM。这套系统的名字拆开来就是desk工作台加comm协同沟通定位很明确给坐办公室的销售和客服团队用的、以协同跟进为核心的桌面端CRM。这篇文章适合正在纠结CRM到底买现成的还是自己开发的团队也适合想了解一套业务系统从需求拆解、技术选型到上线维护全过程的开发者。我会从定位取舍、架构设计、数据建模、权限实现、核心功能落地、故障排查到运维经验把整个过程摊开来讲。文中涉及的代码和表结构都是实际验证过的方案你可以直接参考复用。1. 为什么放弃现成CRM而自己动手DeskcommCRM的定位与取舍1.1 当时业务侧的真实痛点先把业务现状摆出来。我们的销售流程不算复杂市场部获取线索分配给销售销售添加客户、记录沟通、推进报价、赢单后转给交付团队。这个流程里每一个环节都有信息记录的需求但当时没有任何一个统一工具承载这些信息。具体痛点有三个。第一是客户档案割裂同一个客户可能同时躺在三个销售的Excel里报价版本完全不同。第二是跟进过程不透明管理者看不到销售是否真的在跟进、跟到了哪一步只能靠月会汇报。第三是客户资源无法有效复用销售离职后客户资料直接丢失交接文档写得不完整新接手的人要从头了解背景。这三点每一条都直接影响了收入和交付效率。丢单虽然不能全归咎于缺少CRM但没有数据支撑的管理确实让很多问题被掩盖了。1.2 市面产品与业务预期之间的落差试用过几款主流CRM之后我把问题总结为两类。第一类是功能过重。那些主打大型企业解决方案的CRM包含了复杂的报价引擎、合同审批流、售后工单、多级分销管理甚至还有市场营销自动化。我们团队只需要客户管理、跟进记录、数据看板、简单的权限隔离那些高级功能不仅用不上反而让页面变得极其臃肿销售打开系统第一步都不知道点哪里。第二类是数据主权顾虑。核心客户数据全部放在第三方平台一旦合作到期或厂商调整策略数据导出迁移都是麻烦事。对于体量不大的团队来说与其把命脉交给别人不如自建一套可控的系统存在自己服务器上备份策略自己定。另外我算过一笔账主流SaaS CRM按坐席收费假设20个账号一年下来不少钱足够我们雇外包或自己花时间开发两遍。于是自研方案的性价比一下子就出来了。1.3 DeskcommCRM 的功能边界划定自己开发最怕的就是需求失控。我和业务负责人反复对齐后把第一版的功能边界严格锁定在五个模块客户档案管理、联系人管理、跟进记录、销售看板、员工与权限管理。至于合同管理、回款管理、工单系统全部通过导出报表线下处理或者后期以插件方式扩展。这么做的好处是开发周期可控。团队用业余时间加少量全职投入三个月内就上了第一版。我始终认为小团队的自研系统一开始不要追求大而全先把最痛的数据集中问题和跟进透明问题解决掉用户才愿意把系统用起来。系统被用起来之后才有资格谈迭代。2. 技术栈组合与总体架构我是怎么把系统拼起来的2.1 后端选型FastAPI 与 SQLAlchemy 的组合逻辑后端我选了 Python 生态里的 FastAPI 加 SQLAlchemy 2.x。FastAPI 的优势是开发效率极高基于类型提示自动生成 OpenAPI 文档前端同事可以直接在 Swagger 页面上看到所有接口参数后期联调省了大量沟通成本。异步支持也让它在处理导入导出这类IO密集任务时有更好的表现。SQLAlchemy 用 ORM 模式刚开始写的时候有个适应成本但一旦把模型类定义好增删改查基本都是声明式的。尤其后面做数据权限过滤时SQLAlchemy 的动态查询条件构造非常灵活比拼字符串 SQL 安全得多。需要说明的是整个过程我刻意没有引入重型的消息队列或微服务框架。DeskcommCRM 的用户量级就是二三十人并发峰值了不起几十个请求单体应用加关系型数据库完全撑得住。架构不是越复杂越好够用、好维护才是这个阶段的核心指标。2.2 前端与桌面工作流的衔接Vue 3 Element Plus前端选了 Vue 3 配合 Element Plus 组件库原因很朴素团队里没有人是专业前端出身Element Plus 的表单、表格、日期选择器、弹窗等组件开箱即用稍微调一下样式就能达到内部系统的及格线。页面结构上我设计了左侧导航栏加右侧内容区的经典布局。导航栏按业务模块划分工作台当日待办与最近跟进、客户管理、跟进记录、看板统计、系统设置。工作台其实是销售每天打开系统第一眼看到的页面我把今日待跟进客户和最近3天新增客户放在最显眼的位置降低销售使用系统的思考成本。有一个比较关键的设计细节所有列表页都支持高级筛选和列配置记忆。销售可以根据自己的习惯隐藏不关心的列筛选条件会保存到本地存储下一次登录还是同样的视图。这种体验上的小优化对于内部系统的用户留存帮助非常大。2.3 数据库与部署方式数据库用了 MySQL 8.0存储引擎 InnoDB字符集统一 utf8mb4。选择 MySQL 而不是 PostgreSQL纯粹是因为团队对这个生态更熟悉云服务器上装起来也方便。对于这套系统的数据量级MySQL 的性能完全不是瓶颈。部署方式采用了 Docker Compose 编排三个容器Nginx静态文件与反向代理、后端服务Uvicorn 运行 FastAPI、MySQL。这样做的好处是一台 2核4G 的云服务器就能跑起来迁移时只要把整个目录打包拷贝到新机器docker compose up -d一条命令拉起全部服务。2.4 整体目录结构与一次请求的走向后端目录结构大致如下deskcomm-crm/ ├── app/ │ ├── main.py # FastAPI 入口 │ ├── models/ # SQLAlchemy 模型 │ ├── schemas/ # Pydantic 请求/响应模型 │ ├── api/ # 路由层 │ ├── services/ # 业务逻辑层 │ ├── core/ # 配置、安全认证、权限依赖 │ └── utils/ # 导入导出、日期处理等工具 ├── frontend/ # Vue3 项目 ├── docker-compose.yml └── deploy/一次完整的请求链路大概是这样的浏览器发起登录请求Nginx 将/api前缀的请求反向代理到后端容器Uvicorn 接收请求后交给 FastAPI 路由路由函数通过依赖注入获取当前用户对象经过权限校验后调用 service 层执行业务逻辑service 层操作 SQLAlchemy 模型最后以 JSON 格式返回前端收到数据后渲染页面。整体链路非常直白出了问题也好定位。3. 客户数据的根基核心表结构与字段设计3.1 客户主表与联系人表的拆与不拆客户和联系人的关系在业务上其实是一对多一个公司客户下面可能有采购负责人、技术负责人、财务对接人等多个联系人。如果把联系人直接做成客户表里的多个字段一方面字段数量不可控另一方面后续做联系人维度的筛选统计会很痛苦。所以我设计了customer和contact两张表。customer表只存公司维度的核心信息客户名称、行业、客户来源、所属销售、客户状态线索/跟进中/已赢单/已流失、创建时间、更新时间。contact表存联系人姓名、职位、电话、微信、邮箱、备注并通过customer_id外键关联到客户。这个拆分带来的直接好处是销售在客户详情页可以看到这个客户下的所有联系人切换联系人时沟通记录不会混乱。后期如果需要按联系人维度做二次营销直接查 contact 表就能拉到完整名单不用解析分隔符字段。3.2 跟进记录表为什么单独成表而不是堆在客户表里跟进记录是销售日常使用频率最高的数据每一次电话、每一次拜访、每一次微信沟通都可能产生一条记录。如果把这些记录直接追加在客户表的一个备注字段里会出现两个问题第一是字段无限膨胀性能下降第二是无法按时间维度统计分析。follow_up表的核心字段包括customer_id、contact_id本次跟进的联系人、user_id跟进人、content跟进内容、next_time下次跟进时间、status本次沟通阶段、created_at。每次跟进后销售选择当前所处的销售阶段系统自动更新客户主表的状态字段同时生成一条待办提醒。这里有一个很重要的优化跟进列表必须按customer_id和时间倒序查询所以在建表时我加了组合索引(customer_id, created_at)。没有这个索引的话客户详情页加载跟进历史会随着数据量增加越来越慢实战中这是最容易忽视的坑。3.3 业务字典与自定义字段的取舍第一版设计中我就预料到业务方会频繁调整下拉选项比如新的客户来源渠道、新的行业分类。所以没有把这些枚举值硬编码在代码里而是建了一张dict_item数据字典表用dict_type加item_value、item_label描述一条字典项。自定义字段的需求也遇到了。销售提出想加一个记录客户预算范围的字段如果每次都改表结构加列后期维护会非常痛苦。我的处理方案是在customer表上预留一个extra_dataJSON 字段自定义字段以键值对形式存储。JSON 字段在 MySQL 8.0 中已经支持很好的查询能力内部系统完全够用。需要注意的一点是JSON 字段不适合放高频查询条件所以自定义字段只做展示和少量筛选核心筛选仍然走固定字段。4. 权限模型不是非黑即白角色与数据可见范围的设计4.1 三种基础角色与两个特殊角色权限设计是CRM系统里最容易踩坑的部分因为一个简单的谁能看什么数据背后藏着好几层规则。DeskcommCRM 第一版定义了三个基础角色普通销售、销售主管、系统管理员。普通销售只能看自己和客户的数据销售主管可以看自己团队所有销售的数据系统管理员不做业务负责账号管理、字典配置和系统维护。另外两个特殊角色是市场专员和只读访客。市场专员负责线索导入可以新增客户但无权编辑其他销售的客户只读访客主要给老板和财务看数据全部操作都是灰态的只能看统计看板和客户列表。实际使用中只读账号的查询请求占了不小比重所以我在后端单独用了只读数据库账号连接配置从根本上杜绝了误操作的风险。4.2 数据范围控制的落地方式数据可见范围我封装成了一个依赖函数叫get_visible_customer_ids()核心逻辑是根据当前用户的角色动态生成一个客户ID的子查询条件普通销售customer.owner_id current_user.id销售主管customer.owner_id in (下属id集合)管理员和只读访客不限制返回全部客户实现时最关键的一点是这个过滤条件必须注入到所有查询客户列表、客户详情、统计看板的接口中不能只在前端做控制。前端控制只是隐藏入口真正的数据隔离必须在后端SQL层面完成。我在开发时把这段逻辑抽成了公共函数避免每个接口重复写判断也降低了漏加条件的概率。4.3 团队撞单场景下的归属与转移规则销售团队最怕撞单。我在DeskcommCRM里设计了一套简单的归属规则客户创建时自动归属于创建人只有管理员和主管可以手动转移客户归属人一旦客户被某销售锁定其他销售看到这个客户时只能查看不能编辑编辑按钮直接置灰。实际运营中还出现了一个需求销售离职后客户批量转交给指定同事。这项功能我单独做在管理后台管理员选择离职员工一键将该员工名下所有客户批量转移给另一位销售同时跟进记录保留原跟进人信息新销售能看到历史轨迹。这个功能挽救了大量因人员流动丢失的客户数据算是我认为整个系统里性价比最高的一个功能。5. 核心功能模块的实现细节建档、跟进、看板与导入导出5.1 客户建档与唯一性校验客户建档是指销售手动添加或市场专员批量导入客户。手动添加时前端做基础校验比如客户名称不能为空、电话格式需要匹配。后端接口里我额外加了一道唯一性检查逻辑防止同一个客户被重复录入。唯一性规则我设置了两种匹配方式精确匹配客户名称模糊匹配客户名称加联系人手机号。只要命中任意一种系统就提示疑似客户已存在并展示匹配列表由销售确认是否继续创建。这个逻辑减少了大量重复数据后续清洗数据时省了很多功夫。建档流程中遇到的另一个细节是客户状态机的初始化。新客户默认进入线索阶段销售第一次跟随后根据实际情况改为跟进中赢单后由销售手动更新为已赢单并填写赢单金额。赢单金额字段只在这一步开放编辑避免销售事后再改数据影响统计口径。5.2 跟进记录的时间线呈现与提醒机制客户详情页我设计成了上下两块上半部分是客户基本信息和联系人列表下半部分是跟进时间线。每一条跟进记录在时间线上展示跟进人、跟进时间、沟通内容和下一步计划视觉上像聊天记录一样按时间倒序排列。销售打开详情页就能快速了解这个客户的前因后果不用翻聊天记录和邮件。提醒机制是系统活跃度的关键。每天上午九点一个后台定时任务会扫描所有客户最近的跟进时间和设置的next_time把超过三天没有跟进记录的客户汇总成待办列表推送到销售的工作台页面。这个功能上线后销售被动的养成了登录系统查看待办的習慣客户跟进间隔明显缩短。需要说明的是定时任务我用的不是 Celery而是 APScheduler 挂在 FastAPI 进程里。数据量小的阶段完全够用而且少维护一个队列组件部署成本低很多。5.3 数据看板从原始SQL到可视化图表的取舍看板模块是管理层最看重的功能主要包括四个指标新增客户趋势、销售漏斗转化率、各销售业绩排名、客户状态分布。前端选了 ECharts 图表库后端提供聚合查询接口返回JSON数据。第一版我全部用原生SQL写聚合统计比如统计销售漏斗时一个GROUP BY加条件计数就能拿到每个阶段的客户数。对于表数据量在十万以内的小系统,这种方案简单直观、性能足够。这里要提一个我从实践中总结的原则看板的数字必须和列表页的可下钻明细能对上。我遇到过几次业务方质疑看板显示新客户50个点进列表只有49个最后排查发现是看板统计的时候没有排除测试数据列表页却过滤了。后来我把所有统计接口和列表接口的过滤条件统一封装成同一个函数才彻底解决这个对不上的问题。5.4 Excel导入导出的实用细节导入导出是CRM系统里高频需求也是最容易出幺蛾子的地方。导入方面市场专员经常拿到第三方服务商提供的线索Excel表头不规范、存在合并单元格、手机号被存成了科学计数法。我的处理方案是后端用 pandas 加 openpyxl 引擎读取xlsx文件读取后按模板列名映射到数据字典字段。导入前先做两遍校验第一遍检查字段格式和必填项第二遍做重复客户检测。任何一条数据校验失败整批次都不入库系统输出错误报告告诉用户第几行哪个字段有问题。这个全有或全无的导入策略虽然严格但能有效避免脏数据混入。导出功能则相对简单前端点击导出后后端异步生成Excel文件上传到临时文件目录然后把下载链接返回给前端。生成的文件名带上当前日期和导出人方便归档。6. 上线后踩过的三个典型故障完整排查链路记录6.1 故障一导入Excel中文乱码的根因不是文件编码系统上线第一周就遇到了一个让人抓狂的问题导入的Excel文件里中文全部变成乱码但同一个文件用WPS打开一切正常。当时第一反应是文件编码问题于是把读取逻辑从utf-8改成gbk结果报错改回utf-8依然乱码。后来我单独写了一个测试脚本只读取前几行数据并打印出来看发现程序拿到的内容本身就是正常的只有通过API返回给前端才乱。顺着这条线索检查后端响应头发现少设了Content-Type: application/json; charsetutf-8。数据库和文件处理都没问题问题出在HTTP响应编码声明上浏览器只能靠猜测来渲染猜错了就显示乱码。修复方式很简单在 FastAPI 的响应中间件里统一加上编码头。这个故障提醒我遇到乱码不要一开始就怀疑文件系统编码要先从全链路的数据流排查每一层是否都明确指定了编码。6.2 故障二数据权限的幽灵越权问题上线一个月后有销售主管反馈说能看到另一位主管名下客户的数据但权限配置里明明只给了自己团队的可见权限。这个属于严重的安全隐患我立刻开始排查。我把问题拆成三步。第一步复现让主管在系统里实际操作确认确实能看到越权数据。第二步跟踪SQL日志发现有一次查询客户列表时根本没有注入权限过滤条件。第三步定位到代码原来这个查询走的是另一个旧的接口函数它是早期原型阶段写的一直忘了统一改造所以漏掉了get_visible_customer_ids()依赖。这次修复后我做了两件事一是为所有客户相关接口写了一个自动化测试逐个验证不同角色请求时返回的数据条数二是加了SQL日志审计每次查询都把用户ID和角色打印出来方便问题追溯。数据权限这种问题发现一次就要从机制上杜绝二次发生不能靠人肉检查代码。6.3 故障三销售漏斗统计越来越慢的性能问题使用三个月后看板里的漏斗统计接口响应时间从最初的200毫秒涨到了5秒以上。看板页面的体验变得难以接受。我先用慢查询日志定位到具体的SQL语句然后用EXPLAIN查看执行计划发现问题出在customer表状态统计时全表扫描而且follow_up表关联查询没有走到索引。原因很清楚数据量上来后原来的单列索引已经无法满足关联条件的过滤需求。优化方案是给customer表的status字段和owner_id字段建立联合索引给follow_up表的customer_id和created_at建立联合索引同时把漏斗统计的SQL从每状态分次查询改成一次GROUP BY聚合。调整后接口响应时间降到300毫秒左右问题彻底解决。这次经历让我意识到内部系统的索引设计也要提前规划不能等到响应慢了再来处理。数据量超过一万行时索引和慢查询分析就应该成为常规开发流程的一部分。7. 运维经验备份、监控与持续迭代的一些建议7.1 低成本备份方案CRM里全是核心客户数据备份不能省但又不愿意为此增加太多成本。我的方案是每天凌晨两点用mysqldump导出整个数据库备份文件保留最近7天份再同步到另一台存储服务器做异地冗余。恢复演练我也做过两次把备份导入一台临时数据库实例验证数据和功能完整确认流程可用。备份脚本本身用crontab调用非常简单。关键点是每天备份完成后要检查备份文件的大小是否正常如果某天出现0字节或者异常小要立刻发告警通知。7.2 监控告警的轻量做法内部系统不需要上复杂的监控平台我用了两个轻量工具一个是Uptime Kuma对系统首页和登录接口做HTTP探测每两分钟一次挂了就通过微信通知另一个是后端的错误日志上报FastAPI捕获到未处理异常时写入独立的错误日志文件我用一个简单的定时脚本扫描新增错误并发送通知。这套组合几乎零成本但能保证系统出问题时我能在15分钟内知道。对于小团队自研系统来说这个响应速度已经足够。7.3 后续迭代方向DeskcommCRM 第一版上线后团队的使用率从最初的将信将疑到后来每天每人至少打开十次算是达到了预期目标。后面迭代方向我排了几个优先级按联系人做批量邮件营销、客户合同附件管理、移动端H5适配。最急需解决的其实是移动端的访问体验销售在外出拜访时经常需要随手查客户资料目前的PC端页面在手机上操作很别扭。如果让我重新从零搭一套这样的系统我会在第一步就明确先跑通核心闭环再锦上添花的节奏不要被业务方各种新想法牵着走。软件开发最怕的不是技术难度而是需求蔓延。边做边和业务方确认让用户觉得系统听得懂他们的需求比任何华丽的技术架构都重要。这套系统给我最大的体会是内部管理工具的成功本质是流程共识和技术落地的结合工具只是把已经达成共识的业务规则固化了而已。