资讯详情

从Excel到自研CRM:需求分析、技术选型与数据迁移实践

📅 2026/10/19 9:18:40 | 华诺云谱 👁 阅读
从Excel到自研CRM:需求分析、技术选型与数据迁移实践
1. 为什么我在Excel崩溃之后决定自己写一个CRMDeskcommCRM这个名字听起来像个正经商业产品实际上是我一个人咬着牙写了将近两个月的东西。起因很朴素我们团队一直用Excel管客户从最开始50个客户没问题到后来300多个线索、40多个进行中的商机、十几个人要共享同一份表格Excel开始频繁卡死、版本错乱、甚至有一次同事误删了一整列跟进记录气得我当场想砸电脑。市面上的CRM产品其实不少功能也确实全但问题也很明显。一个是贵按坐席收费一年下来小几万对小团队来说是一笔不小的开销另一个是重Salesforce那种级别的配置项能把人绕晕我们只需要管好线索、客户、跟进和成交根本用不上那些复杂的自动化流程。更麻烦的是数据在别人服务器上每次想到客户资料不在自己手里心里总有点不踏实。所以当时我的判断是与其花几个月去适应一套本就为大型销售组织设计的成熟系统 不如自己动手用最少的功能集解决我们最痛的问题。DeskcommCRM就这么立项了目标只有一个——把客户数据管起来让每个销售清楚地知道自己手头有哪些线索、哪些客户该跟进、这个月离目标还差多少。这篇文章不聊概念纯聊实操。我会把当时的需求决策、技术选型、核心模块设计、数据迁移过程以及上线之后踩过的坑全部摊开来讲。如果你也想给团队做一套内部管理系统或者只是好奇一个从零开始写的业务系统长什么样这篇内容应该能给你一些实实在在的参考。2. 技术选型与整体架构不追新只追够用2.1 后端、前端和数据库我为什么这么选写业务系统最忌讳的就是为了炫技而上一堆复杂架构。我的第一原则是团队里谁能维护就用什么。我们团队没有人专职做运维所以越简单的部署方式越好。后端我选了Python的FastAPI。原因很直接异步性能足够、自带OpenAPI文档、写起来代码量少一个人维护很轻松。对比DjangoFastAPI没有自带Admin后台和ORM需要自己配SQLAlchemy但换来的是更清晰的请求链路和更轻量的运行时。前端选了Vue 3加Element Plus。这套组合的好处是组件丰富表格、表单、弹窗、日期选择器统统开箱即用适合做后台管理类的工具。我没有上TypeScript团队里有人不熟而且项目规模不大纯JavaScript反而能把代码写得更容易懂。数据库用的PostgreSQL。坦白说如果只是存客户和商机MySQL也完全够用但PostgreSQL对JSON字段的支持要好得多。我们在跟进记录里需要存一些动态的扩展属性用JSONB来存后续想要加字段也不用改表结构省了不少麻烦。部署就更是怎么简单怎么来。一台2核4G的云服务器装好Docker和Nginx前后端各自跑一个容器数据库和数据卷挂在本机磁盘上每天凌晨自动备份一次。整个部署过程写成一个脚本跑完不超过十分钟。2.2 目录结构与模块拆分项目虽然小但目录结构不能乱。我参照业界惯用的分层方式把后端按功能模块拆开而不是按技术类型硬切。这样做的好处是当你新增一个功能时只需要找到对应的业务包在里面加代码就好。deskcomm-crm/ ├── backend/ │ ├── app/ │ │ ├── api/ # 路由层 │ │ │ ├── leads.py │ │ │ ├── customers.py │ │ │ ├── opportunities.py │ │ │ └── activities.py │ │ ├── models/ # 数据库模型 │ │ ├── schemas/ # 请求/响应模型 │ │ ├── services/ # 业务逻辑层 │ │ └── core/ # 配置、安全、工具函数 │ ├── tests/ │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── api/ # 前端请求封装 │ │ ├── views/ # 页面组件 │ │ ├── components/ # 通用组件 │ │ └── router/ # 路由配置 ├── docker-compose.yml └── deploy.sh模块划分上我一开始就定下了五个核心业务域线索管理、客户管理、商机管理、跟进记录、报表统计。每个业务域对应的是一组API加一个前端页面彼此之间通过外键关联但逻辑上保持独立。这里我想特别提醒一点小项目最容易犯的错是到处写全局变量和跨模块调用当时图方便后面改需求的时候想死的心都有。保持模块边界的干净是长期维护的基础。2.3 权限模型设计CRM里最敏感的就是权限问题。谁能看到全部客户谁能修改数据谁只能看自己的这些必须在一开始就想清楚不然后面改权限是极大的工程。我没有引入复杂的RBAC框架而是用了一个最简单实用的方案角色加数据范围。角色分三种——管理员、销售主管、普通销售。数据范围也分三种——仅本人、本部门、全部。每个API在入口处做一个装饰器校验先检查用户角色有没有权限访问这个接口再检查数据范围能不能覆盖目标数据。比如普通销售修改客户时查询语句强制加上了 created_by 等于当前用户的过滤条件。这样就算前端绕过界面直接调接口后端也会拦住。# 数据权限过滤的核心思路 def get_accessible_customers(db, current_user): query db.query(Customer) if current_user.role admin: return query.all() if current_user.role manager: return query.filter(Customer.department current_user.department).all() return query.filter(Customer.created_by current_user.id).all()这个设计我用了半年多没有再改过。前期多花半小时梳理的权限边界后期少花好几天去修复数据泄露的问题。3. 核心模块设计与实现细节3.1 线索模块从“听到消息”到“建立客户”线索是整个CRM的入口。我们的大部分客户来源是官网留资、行业展会、转介绍和被竞争对手忽略的沉默客户。线索的特点是不确定可能是一张名片、一个电话也可能只是一封没头没尾的邮件所以它和正式客户要分开管理。线索表我设计了这些字段公司名称、联系人姓名、电话、邮箱、来源渠道、线索状态、负责人、备注。其中线索状态是整个模块的核心我用了一个状态机来驱动而不是简单地存一个字符串。new新线索→ contacted已联系→ qualified已验证→ customer转客户 ↘ disqualified已失效为什么要用状态机因为普通字符串字段可以随便改但状态机强制规定了流转路径。比如一条线索不能从“新线索”直接跳到“转客户”中间必须经过“已联系”和“已验证”。这看起来增加了操作步骤却能逼着销售把信息补完整再进入下一阶段。前端页面上线索列表默认按创建时间倒序支持按来源和状态筛选点击某条线索右侧弹出详情抽屉可以直接在上面写跟进、改状态、转给其他人。整个交互我参考了Trello的轻量操作思路尽量减少页面跳转。3.2 客户与商机数据建模怎么做到既灵活又规范线索验证过后就转成客户。客户表是这个系统的核心几乎所有业务都围着它转。但我吸取了之前Excel的教训没有把所有字段都堆在同一张表里。客户的基本信息分成两个层次公司级别的信息公司名、行业、规模、地址、评级、来源放主表联系人的信息姓名、职位、手机、微信、邮箱单独建了一张联系人表。一个客户可以对应多个联系人这符合真实业务情况。以前Excel里一个客户占一行多个联系人只能挤在一个单元格里真正要用的时候根本没法拆出来做筛选。商机模块管理的是客户背后的具体销售机会。一个客户可能有多个商机每个商机对应一笔可能的成交。商机表的字段是客户ID、商机名称、预计金额、成交概率、预计结单日期、销售阶段、负责人。销售阶段我沿用了常见的漏斗模型初步接触、需求确认、方案报价、商务谈判、赢单、输单。每个阶段对应一个默认的成交概率比如初步接触是10%需求确认是30%方案报价是50%商务谈判是70%赢单是100%。这样月底统计预测收入时直接用金额乘以概率就能算出加权预测额。数据库表设计上客户和商机之间是一对多关系商机和跟进记录是一对多关系。跟进记录属于活动流的一部分我放在下一小节单独说。3.3 跟进记录与活动流让每一个动作都有迹可循跟进记录是CRM里使用频率最高的功能也是我做的最细的模块。每一条跟进记录包含关联的客户ID、关联的商机ID可空、跟进方式电话、见面、邮件、微信、跟进内容、下次跟进日期、实际跟进人、记录时间。为什么这个模块值得花心思因为销售管理本质上就是在管理行为和结果之间的因果关系。一条商机为什么赢了为什么输了翻一翻跟进记录就能还原全过程。如果这个字段做得太简单后面做复盘时什么都查不出来。我在设计跟进记录时还做了两个小功能。一个是“下次跟进日期”生成后自动写入待办事项销售每天打开首页就能看到今天该联系谁另一个是把同一客户下的所有跟进记录按时间轴展示视觉效果类似朋友圈每条记录会显示是谁在什么时候写的。# 保存跟进记录时自动更新客户/商机的最近联系时间 def create_activity(db, activity: ActivityCreate): db_activity Activity(**activity.dict()) db.add(db_activity) if activity.opportunity_id: db.query(Opportunity).filter(Opportunity.id activity.opportunity_id).update( {last_contact_date: datetime.date.today()} ) db.commit() return db_activity这个逻辑虽然简单但带来的体验提升非常明显。销售打开客户详情页一眼就能知道这个客户上一次接触是什么时候、聊了什么、接下来该做什么不用再到处翻聊天记录和邮件。3.4 仪表盘与销售漏斗用数据代替“感觉”做完上面几个模块后系统已经能帮助团队管理日常工作了但真正让它价值翻倍的是最后加的仪表盘模块。老板关心的是结果销售关心的是过程而仪表盘能同时满足两种视角。仪表盘我做了四个卡片加两张图。四个卡片分别展示本月新增线索数、本月新增客户数、本月成交金额、本月待跟进任务数。两张图里一张是销售漏斗图展示当前所有商机在各阶段的分布和金额总和另一张是折线图展示最近六个月的成交金额趋势。漏斗图的实现并不复杂本质就是一个按阶段分组的聚合查询from sqlalchemy import func def get_pipeline_report(db): rows ( db.query(Opportunity.stage, func.sum(Opportunity.amount).label(total)) .filter(Opportunity.status open) .group_by(Opportunity.stage) .all() ) return [{stage: stage, total: total} for stage, total in rows]折线图则按月份聚合先算出最近六个月每个月的赢单金额总和再交给前端渲染。这些数据过去靠手工统计至少要半天现在打开页面实时刷新。4. 从Excel搬家数据迁移的实战记录4.1 清洗Excel从混乱到规范系统写完之后最头疼的不是写代码而是把旧Excel里的数据搬进新系统。现实中的Excel远比想象中混乱公司名有时候带后缀有时候不带电话号码里有括号有横杠还有的写着“转123”同一个客户被不同人录了两遍字段还不一样。我写了一个专门的清洗脚本用pandas读取Excel然后逐列处理。公司名做去除多余空格和统一大小写处理手机号统一去掉所有非数字字符邮箱转小写。清洗完的数据生成一个新的DataFrame导成CSV再交给导入脚本使用。提示清洗这一步骤千万别省。脏数据一旦进入新系统后续排查和清理的成本会成倍增加。宁可多花一天洗数据也不要带着垃圾数据上线。4.2 分批导入与幂等性校验导入时我采用分批导入的策略每批100条分批插入数据库并对每一条结果做校验。为了确保导入不出错我在导入脚本里实现了两个关键机制一个是幂等性校验通过唯一字段比如公司名加联系电话的组合哈希判断这条记录是否已经导入过另一个是事务回滚如果某批数据里有导入失败的主记录该批全部回滚并输出错误行信息。import csv from sqlalchemy.orm import Session def import_customers(session: Session, csv_path: str): with open(csv_path, encodingutf-8-sig) as f: reader csv.DictReader(f) batch [] for idx, row in enumerate(reader): batch.append(row) if len(batch) 100: _process_batch(session, batch) batch [] if batch: _process_batch(session, batch) def _process_batch(session, batch): try: for row in batch: customer Customer( namerow[公司名], phonerow[联系电话], emailrow[邮箱], sourcerow[来源] or 历史导入, created_byDEFAULT_USER_ID, ) session.add(customer) session.commit() except Exception as e: session.rollback() print(f批次导入失败已回滚错误信息{e}, 数据行数{len(batch)})迁移完成后我在系统里随机抽样了50条数据和原Excel逐条比对重点检查公司名是否完整、联系电话是否缺失、负责人是否归位。另外用脚本统计了总条数和Excel实际行数做了一致性校验。比对通过之后才正式通知团队可以停止Excel流程全部改用系统。5. 上线之后踩过的坑以及最终的效果复盘5.1 时区、并发和移动端踩得最深的三个坑上线初期问题确实不少。第一个坑是时区问题。后端服务器默认用UTC时区前端用户在中国录入一条18点30分的跟进记录存进数据库变成10点30分第二天查看时日期都变了。排查过程很痛苦最后发现是SQLAlchemy创建连接时没有指定时区。解决办法是在数据库连接串上加上时区参数同时在写入和读取时统一转换为本地时间。第二个坑是并发更新导致的数据丢失。两个销售同时打开同一个客户详情页A改完手机号保存B改完地址保存后保存的那次操作会把先保存的整行数据覆盖掉。我没有完全解决这个问题而是做了一个折中的优化在更新接口里增加了updated_at字段的乐观锁判断如果提交时的updated_at和数据库当前值不一致直接返回冲突提示让用户刷新后再修改。第三个坑是移动端体验。团队销售经常在外面跑客户回来后要在手机上记录跟进内容。虽然前端用了响应式布局但Element Plus在手机上的表格体验确实一般。我后来做了一个简化版的操作页面首页展示待办事项、今日要跟进的客户列表、快速写跟进入口。这个页面只用卡片和按钮不依赖表格手机上的操作效率反而更高了。5.2 销售不愿意用的解法任何管理工具最大的敌人是团队不愿意用。我们刚上线时就有两个销售强烈抵触觉得每次跟进都要打开系统填一遍比Excel还要麻烦。我的处理方法很简单每周例会不拿Excel说事了只拿系统里的数据说话。谁这个月的跟进次数不够、谁的商机处于停滞状态、谁的预测成交额落后所有数据都直接从系统里打出来贴在白板上。数据透明之后大家发现不填系统等于自己挖坑。毕竟月底复盘、奖金核算、客户归属判定都以系统数据为准。两周之后系统的日活数据基本稳定了下来销售开始主动去完善历史客户的跟进信息。最终的效果上线三个月后我们做了个简单统计客户数据从Excel时期丢失和重复带来的混乱到系统里零丢失销售例会准备时间从原来的一下午缩短到十分钟月底销售预测的准确率从差不多靠猜提升到了80%以上。这些数字不是产品能力的证明而是记录了一个团队从不信任工具到依赖工具的转变过程。5.3 代码之外的一些建议最后再说两个维护上的小建议。一个是备份我在服务器上挂了定时任务每天凌晨3点用pg_dump备份数据库保留最近30份同时把备份文件同步一份到对象存储。系统可以挂数据不能丢这是我用真金白银换来的教训。另一个是变更管理。小团队没有规范的测试流程但至少要保证每次上线前在本地把核心流程跑一遍登录、新建线索、转客户、建商机、记录跟进、查看仪表盘。我吃过一次亏前端加了字段后忘记改接口校验规则结果销售录单时频繁报错折腾了一下午。DeskcommCRM到现在已经运行了半年多代码量不大功能也不算多但它刚好解决了我们团队真正的问题。每次有人问我要不要买一套商业CRM我的回答都是看需求复杂度。如果你的流程也很简单需求也很明确完全可以考虑自己动手做一套。这半年维护下来我最大的收获不是代码而是理解了业务系统和真实工作流程之间的关系——真正好用的系统从来不是功能堆出来的而是一点点贴合用户习惯打磨出来的。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑