尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零自建轻量级CRM系统:技术选型、数据模型与权限实战

从零自建轻量级CRM系统:技术选型、数据模型与权限实战 1. 为什么做DeskcommCRM免费公共CRM与私有自建的本质差异先说明一下DeskcommCRM是什么。它是我们团队内部从零搭起来的一套轻量客户关系管理系统名字里的Deskcomm可以理解为“桌面沟通”核心定位是让销售和客服人员每天打开电脑就能看到该跟进的客户、该处理的工单、该填写的记录而不是在Excel和微信聊天记录之间来回折腾。这个项目的起因挺朴素。团队规模到了十几个人之后客户信息开始分散在销售个人微信、手机通讯录、Excel表格、甚至纸质名片里。每次要统计客户来源、跟进状态、成交转化都要找每个人单独要数据整理出来的结果还经常对不上。市面上不是没有现成的免费CRM我也认真试过好几款但用着用着就发现一个核心问题免费公共CRM的数据都在别人服务器上功能做得越全权限边界越模糊你越担心自己的客户资源哪天被平台规则影响。“永久在线的网站CRM”和“私人自建CRM”有什么区别其实很多人搜过这个问题。官方说法是前者你不用管服务器、不用管维护打开浏览器就能用但实际用下来免费公共CRM通常有客户数量限制、字段定制受限、导出数据要付费、API调用频次卡得死死的。而私人自建这套方案数据在自己手里字段想怎么改就怎么改员工账号想怎么配就怎么配唯一代价是前期要投入一些搭建时间。所以我当时给DeskcommCRM定的目标很明确不追求大而全只做四件事——管住客户线索、记录跟进历史、展示销售漏斗、配置团队权限。系统不需要多花哨但必须长期可维护数据必须能完整导出员工第二天就能上手。2. DeskcommCRM的整体设计与核心模块拆解2.1 模块设计先想清楚数据怎么流动动工之前我先把业务流程画了一遍不是画系统架构图是画业务数据怎么流转。我们团队每天的真实动作是这样的市场部从表单和广告后台收集线索把线索分配给销售销售打电话或者加微信每次沟通后要在系统里留一条跟进记录有购买意向的客户进入商机阶段销售填预计金额和成交时间成交之后客户资料转给客服客服继续维护售后。整个过程看起来不复杂但涉及的人、角色、状态特别多如果不把数据模型定清楚后面写代码就会一直返工。所以我先定义了几条核心数据流线索Lead未验证的潜在客户来源是表单、广告、活动扫码。客户Customer已验证、有明确联系人的对象可能是企业也可能是个人。商机Opportunity客户有了购买意向进入销售漏斗阶段。跟进记录Activity每一次打电话、发微信、见面拜访的时间点和内容概要。工单Ticket成交后的售后服务记录。这五个实体在DeskcommCRM里互相引用形成一条完整的链线索可以转化为客户客户可以建立多个商机商机下面挂跟进记录成交后生成工单。所有数据都用统一的创建人、所属人字段控制可见范围这样权限后面不会乱。2.2 数据模型用最简单的方式建模客户关系数据库设计阶段我刻意做了精简没有用那些复杂的ER模型主要是怕团队里其他人看不懂。整个系统最核心的就是一张客户表和一张跟进记录表其他表都是围绕它们扩展的。客户表设计的核心字段大概是这样CREATE TABLE customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, company TEXT, mobile TEXT, wechat TEXT, source TEXT, owner_id INTEGER, status TEXT DEFAULT new, level TEXT DEFAULT C, next_follow_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这个表里最关键的是owner_id它决定了这个客户归谁管。后续所有权限判断、数据隔离都靠这个字段员工登录系统后只能看到自己名下或者自己参与协作的客户。next_follow_at是我后来加的作用相当于一个提醒机制销售每天打开系统就看到“今天该跟进谁”这是系统使用率提升的关键设计。跟进记录表也不复杂CREATE TABLE activities ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL, user_id INTEGER NOT NULL, type TEXT, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这里没有做复杂的关联表因为初期业务就是一对多一个客户有多次跟进每次跟进记录归属到客户。数据量到几十万条之前这种简单模型性能完全够用。系统跑了一年后我们最大的客户表也就一万多条记录SQLite照样秒开。2.3 技术选型为什么我不选重型框架技术栈这块我一开始也纠结过到底用Java Spring还是直接用PHP后来冷静想了一下团队需求决定用Python加Flask加SQLite的组合前端用简单的模板语法渲染页面数据可视化用ECharts。选择这个组合的原因有几个。第一团队里大部分人会写一点Python出问题能自己排查不需要专门招后端。第二项目体量不大日常并发就是十来个人同时操作SQLite单文件数据库完全能扛住还能直接备份文件省去MySQL的运维成本。第三Flask的代码量比Spring少太多整个系统核心代码不到两千行维护成本低。如果让我重新选一次我还是会选这套方案。不是说Spring不好而是项目规模决定了技术选型。团队内部工具最重要的指标是“能改、能跑、能交付”而不是技术栈多酷炫。那些用Docker把Nginx、MySQL、Redis全安排上的做法对十几人的内部系统来说完全是资源浪费。前端交互我用了很轻量的方案表格用原生HTML加一点JavaScript筛选和排序通过后端GET请求带参数实现。这样每个页面刷新后URL可以分享给同事方便排查数据问题。不需要做单页应用因为内部工具的使用习惯就是打开浏览器、操作、关闭没有人会在一个页面上停留几个小时。3. 实操记录从空目录到第一个可用版本3.1 环境准备与初始化环境准备这一步其实没什么高深的就是把Python、Flask和数据库驱动装好。我用的Python 3.10依赖管理用pip加requirements.txt。整个项目目录结构是这样的deskcommcrm/ ├── app.py # 主入口路由和视图 ├── models.py # 数据库初始化与查询封装 ├── templates/ # HTML模板 │ ├── base.html │ ├── index.html │ ├── customers.html │ ├── customer_detail.html │ └── login.html ├── static/ # CSS、JS └── crm.db # SQLite数据库文件启动之前先在models.py里初始化数据库和表结构这一段代码很简单但很重要以后每次升级只需要在初始化函数里新增建表语句import sqlite3 DB_PATH crm.db def init_db(): conn sqlite3.connect(DB_PATH) c conn.cursor() c.execute(CREATE TABLE IF NOT EXISTS customers (...)) c.execute(CREATE TABLE IF NOT EXISTS activities (...)) c.execute(CREATE TABLE IF NOT EXISTS users (...)) conn.commit() conn.close() if __name__ __main__: init_db()我习惯把建表语句直接写在代码里而不是单独维护SQL脚本这样每次部署完只要运行一次应用就能保证表结构是最新的。对于内部工具开发模式这比维护迁移版本更实用。3.2 核心路由客户列表与详情页初始化做完之后先写的是客户列表页这是整个系统的门面。列表页的查询逻辑其实就是一个带筛选的SQL查询根据当前登录用户ID过滤数据再支持按状态、按来源、按负责人筛选。app.route(/customers) def customer_list(): user_id session.get(user_id) if not user_id: return redirect(/login) status request.args.get(status, ) source request.args.get(source, ) keyword request.args.get(keyword, ) query SELECT * FROM customers WHERE owner_id ? params [user_id] if status: query AND status ? params.append(status) if source: query AND source ? params.append(source) if keyword: query AND (name LIKE ? OR company LIKE ? OR mobile LIKE ?) like f%{keyword}% params.extend([like, like, like]) rows query_db(query, params) return render_template(customers.html, customersrows)这里用了最朴素的字符串拼接加参数化查询避免了SQL注入的同时也保持了代码可读性。很多新手喜欢用ORM但内部工具用原生SQL反而更直白数据量不大性能差异可以忽略。客户详情页的接口我设计成同时返回客户基础信息、商机列表、跟进记录列表三个区块一个页面解决所有信息浏览需求。这个设计思路来自一个很朴素的观察销售打开一个客户档案时希望一眼看完“这个人是谁、聊过什么、还有哪些事没做”而不是在不同标签页之间来回切换。3.3 登录认证与员工邀请权限配置权限管理是CRM绕不开的环节尤其是多人协作之后员工能看到哪些客户、能不能删除记录、能不能导出数据都要限制清楚。DeskcommCRM的账号体系走的是最简单的用户名加密码模式密码用哈希存储不存明文。员工邀请这个功能我单独说一下因为很多商用CRM把这个流程包装得很复杂。本质上邀请员工就是管理员创建一个新账号然后告诉员工初始密码。我用了一个随机密码生成器在后台生成临时密码首次登录后强制修改app.route(/admin/invite_employee, methods[GET, POST]) def invite_employee(): if not is_admin(session.get(user_id)): abort(403) username request.form.get(username) role request.form.get(role) temp_password generate_random_password(10) create_user(username, temp_password, role) return jsonify({username: username, temp_password: temp_password})这个接口比传统邮件邀请更直接因为团队都在同一个办公环境新员工入职时由主管当面把临时用户名密码发给他五分钟内就能登录系统。商用系统里那种“邀请邮件点链接设置密码”的流程在公司内部场景里反而是多余的。对应的权限控制我做了三层第一层是登录权限没有账号连系统都进不去第二层是数据可见范围普通员工只能看自己维护的客户主管可以看整个团队的客户第三层是操作权限删除客户和导出数据只有管理员有权限。这三层分别对应路由守卫、SQL筛选、按钮级控制实现不复杂但全覆盖了实际需求。这里也回应一下飞鱼CRM员工邀请的问题不管飞鱼还是其他商业CRM邀请员工的核心逻辑就是“管理员建账号、分配角色、发密码”只是商业系统往往把这块做成邮件模板和二维码扫码看着高级本质没变。3.4 跟进记录与销售漏斗实现跟进记录是整个系统里被调用最多的功能销售每打一个电话都会进这里。为了避免“忘记写跟进”的情况我在客户详情页强制做了弹窗提醒如果客户的next_follow_at时间已过期打开详情页时顶部会有红色提醒条。提交跟进记录的表单设计得也尽量简单只有三个字段跟进方式电话/微信/拜访、内容描述、下次跟进时间。填完保存之后系统自动更新客户表的next_follow_at和status。这段逻辑的代码大概长这样app.route(/customers/int:customer_id/activities, methods[POST]) def add_activity(customer_id): content request.form.get(content) act_type request.form.get(type) next_follow request.form.get(next_follow_at) user_id session.get(user_id) insert_activity(customer_id, user_id, act_type, content) if next_follow: update_customer_next_follow(customer_id, next_follow) return redirect(f/customers/{customer_id})销售漏斗我实现得相对简单按照商机状态分成“初步接触、需求确认、方案报价、谈判、成交、流失”六个阶段每个客户有一个当前阶段列表页按阶段分组展示形成类似看板的效果。没有做拖拽操作而是用下拉框修改阶段因为拖拽在移动端和触屏设备上不好用而下拉框在任何设备上都能操作。3.5 部署上线让团队真正用起来开发完成之后部署环节我采了最省事的方式一台内网服务器用Gunicorn跑Flask应用Nginx做反向代理SQLite数据库文件放在固定目录每天凌晨备份一次。这套方案不需要公网域名内网IP加端口号就能访问团队出差时再通过公司现有系统做远程访问。部署完上线那天的体验让我印象很深。前一周没人用第二周开始有几个销售主动录入客户第三周基本所有新线索都进系统了。我复盘下来觉得系统真正被接受不是功能多而是有两点做对了一是录入速度快手机浏览器打开页面输入三四个字段就能保存一条客户比打开Excel还快二是每天早上的“今日待跟进”提醒确实帮销售记住了该做什么。如果后续不满足于内网想做成“永久在线的CRM网站”那只需要把服务器暴露到公网并做HTTPS认证数据模型和业务逻辑完全不用变。免费公共CRM和私人网站的差别不在功能而在你愿不愿意承担服务器和运维成本。这个决策没有对错只看数据资产在心里的分量。4. 常见问题与排查技巧速查4.1 客户数据同步和备份问题SQLite单文件数据库的优点是好备份缺点是并发写有锁。初期遇到过几次数据库锁死的情况是两个人同时提交跟进记录时触发的日志里出现database is locked错误。解决办法是给SQLite连接加上超时设置conn sqlite3.connect(DB_PATH, timeout10)另外我把备份脚本写成了一个简单的Shell定时任务每天凌晨用sqlite3 crm.db .backup backup.db命令做在线备份然后同步到另一台机器。数据备份这件事我用一个教训总结CRM的数据丢失不是丢一条记录的事是客户关系链的断裂损失无法量化所以备份宁可多不能少。4.2 权限异常和员工交接员工离职后他名下的客户不能直接删除否则客户资料就彻底丢了。我处理这类情况的方式是先把离职员工的账号停用然后批量把客户owner_id改到主管账号再让主管分配给其他销售。这个操作我用一条简单的SQL完成UPDATE customers SET owner_id 2 WHERE owner_id 5;如果发现有人能看到不属于自己的客户数据大部分原因是之前直接改数据库时漏掉了owner_id条件。排查思路是确认登录用户ID然后再看客户列表页拼接的SQL条件是否包含owner_id过滤。权限这种东西宁可刚开始设严一点后面再逐步放开。4.3 待跟进提醒不生效这个坑是项目管理上最常见的问题。销售按原来的习惯都是在Excel里记录待跟进时间系统上线后没有养成看提醒的习惯所以即使next_follow_at设置得再准还是有人会漏。后来我在主页上做了一个“今日待跟进”的强提醒模块直接把当天需要跟进的客户列成清单放在登录后第一个页面。这还不够又加了每日下班前的统计摘要邮件自动把当天未完成跟进的客户列表发给各销售。这一步效果明显漏跟进的次数少了七八成。给新做CRM的朋友一个建议功能可以后补但“今日待跟进”这种面向行动的设计一定要第一个做它直接决定了使用频率。4.4 多浏览器兼容和手机访问适配的一堆坑内部系统最容易忽视的就是手机访问体验。销售经常在外出路上用手机查看客户资料如果页面是桌面版布局在手机上要放大缩小才能点按钮体验很差。第一版我只做了基础的响应式客户列表在手机上勉强能看但表格列太多左右的滑动操作比较笨拙。后来我把客户列表页在手机端默认只显示姓名、公司、状态三列其余信息点击进入详情再看交互轻了很多。同时“保存客户”“添加跟进”两个高频按钮在手机上固定到了底部悬浮位置单手操作也方便。还有一个小坑值得提一下中文输入法在表单中有时会自动补全出奇怪字符导致搜索不到结果。排查后发现是浏览器自动填充干扰后来干脆给关键表单加上了autocompleteoff属性问题才消停。5. 这次实践下来我的一些真实感受DeskcommCRM从需求梳理到跑起来前后花了一个周末加几个工作日的晚上真正手写的核心代码不到两千行。这个东西的技术含量不算高但给我的启发很大。最大的感受是内部工具真正难的不是写代码而是把你的业务流程抽象成数据模型。我在第一版设计数据模型时多花了几个小时后面写代码和改需求的速度明显快了很多。那些急着写代码、不思考数据关系的项目后面往往都要推倒重来。另外一个感受是系统不是做出来就完了运营比开发重要。CRM是否被团队接受关键在于它是否让每个人的日常工作变简单了。如果我当时先做一个复杂的权限体系、多层级审批流估计上线第一天就被骂了。先跑通核心链路再慢慢加功能是内部工具做法的正确顺序。当初对于“要不要自建CRM”这个问题如果再让我选一次我依然会选自己做。自己搭意味着你随时知道系统里有什么数据、逻辑是怎样的遇到问题能当场修改而不是提工单等对方排期。免费的公共平台确实省心但那是对“客户信息不在乎放哪里”的团队而言如果你跟我一样把这些数据当核心资产那私人系统这条路还是值得走一趟的。以后如果再扩展我可能会加上更多维度的数据统计页面比如按来源分析成交率、按销售对比跟进频次再把工单模块做得更完整一些。不过那个优先级不算高毕竟当下最要紧的是让团队把每天的跟进记录都坚持留下来。
返回列表