首页 / 资讯中心 / 文章详情

基于Python和Vue3的企业客户关系管理系统开发实战

基于Python和Vue3的企业客户关系管理系统开发实战 ★ FEATURED ARTICLE
项目标题里的关键词很直白Python、企业客源、客户关系管理系统、Vue3。这类系统在市面上有很多现成的SaaS但真正落地到一家具体企业时往往会发现通用产品根本没法贴合自己的业务流。我接手过好几个类似的需求最后都绕不开自己动手定制这条路。这篇文章不打算讲那种大而全的理论而是从实际开发者的视角把整个系统的设计思路、技术选型、核心实现和踩坑记录完整梳理一遍给正好准备做类似项目的朋友一个可以照抄的参考框架。我默认你已经有Python和前端的基础但如果看到Vue3的部分觉得陌生也别慌后面会专门讲清楚Composition API和响应式原理在实际业务里的用法。1. 客源管理的真实痛点为什么我决定从Excel迁移到定制CRM1.1 销售团队日常管理客户的三座大山在没有CRM系统之前大多数中小企业的客源管理方式非常原始——销售把客户信息记在Excel里或者干脆写在手机通讯录和微信备注里。这种做法在第一二个客户的时候还能应付一旦客户量超过100个问题就开始集中爆发。最大的痛点是客户归属不清。销售A和销售B同时跟进同一个企业客户到底算谁的业绩Excel没法做有效的去重和查重经常出现两个销售给同一家公司的不同联系人打电话客户被来回骚扰体验极差。我见过最极端的案例一家做企业服务的公司有2000多个客户线索分布在7个销售手里其中超过30%是重复的。第二个痛点是跟进记录断层。今天给客户打了电话聊了什么答应了什么时间再联系全部靠销售个人记忆。人一忙就会忘客户一旦流失复盘的时候根本找不到原因。我之前用的Excel方案里也尝试过建一个跟进日志表但实际使用率很低——销售要复制客户名、切换表格、填写时间整个操作成本太高。第三个痛点是管理者视角缺失。老板想知道本月的线索量、转化率、成交金额、每个销售的跟进饱和度传统方式只能让助理去手动汇总等到数据出来已经是半个月之后早就错过了调整策略的最佳时机。1.2 自研CRM的范围界定针对上面这些问题我在做系统设计之前先列了一个功能优先级清单。这里的核心原则是先解决数据结构化的问题再解决流程自动化的问题。我当时划定的第一版核心范围如下客户档案管理企业客户的完整信息录入包含基本信息、联系人、来源渠道、所属行业、客户等级跟进流程管理每次销售与客户的互动都形成一条结构化记录并自动生成下一次跟进计划商机与合同管理从客户状态中区分出潜在、跟进中、成交、流失等阶段关联到具体商机金额数据看板管理者按日/周/月维度查看线索量、跟进次数、转化漏斗和销售排行权限控制销售人员只能看到自己的客户团队主管可看下属数据老板看全量功能性需求定完非功能性需求也同步明确系统必须支持200人以内团队的同时在线使用响应时间控制在1秒以内数据要能每日自动备份。这些指标听上去不难但在后续的表结构设计和接口优化中它们直接决定了技术方案的取舍。2. 技术选型复盘Python后端加Vue3前端到底怎么搭配最顺手2.1 后端框架对比Flask、Django、FastAPI怎么选先聊后端。Python是标题里写死的技术栈这没问题但Python后端框架的选择会直接影响开发效率。我做了个简单对比框架适合场景优点缺点Flask轻量API服务、单模块应用灵活、上手快、控制粒度细需要自己搭建ORM和认证工程化能力弱Django大型全栈项目ORM和Admin后台成熟、自带认证一站式偏重序列化和权限控制的灵活性差一些FastAPI高并发API服务、前后端分离性能强、异步支持好、自动生成OpenAPI文档生态相对年轻团队需要适应异步风格我的选择是Django之外的FastAPI。原因很简单这个CRM系统是典型的前后端分离项目前端Vue3走的是纯API调用我需要的是一个高性能、自带文档、能快速实现JWT认证的轻量级API层。FastAPI的自动交互文档节省了我大量联调时间这是我在实际开发中体会特别深的一点。当然如果你的团队对Django的生态更熟悉用Django REST Framework也不是不行只是项目结构会显得重一些。Python 3.11版本是我最终采用的运行环境。它比3.8在类型提示和性能上有明显进步而且FastAPI在3.11上跑异步接口的实测并发表现更稳定。2.2 为什么Vue3而不是Vue2前端选Vue3几乎是板上钉钉的事。2026年再做新项目Vue2已经完全进入维护状态新功能不再添加周边生态也基本停摆。Vue3的核心优势不是那点运行时性能提升而是Composition API带来的代码组织能力变革。用Options API写代码逻辑分散在data、methods、computed、watch各个区块里一个功能逻辑需要跨区块跳来跳去地看。Composition API允许你按照功能维度组织代码把客户搜索的逻辑、报表统计的逻辑、权限控制的逻辑分别写成独立的函数组件只负责拼装调用。这种模式下大模块的维护成本直线下降。和Vue3配套的生态链我选择如下构建工具Vite开发环境下冷启动秒级响应热更新比webpack省太多时间UI组件库Element PlusCRM系统大量使用表格、表单、弹窗、标签页Element Plus的成熟度最高状态管理Pinia相比Vuex去掉了mutations概念API更加简洁直接路由管理Vue Router 4配合动态路由实现权限控制2.3 数据库与部署环境选型数据库我选的MySQL 8.0。原因一企业对MySQL的运维经验最普遍后续接手的人不会陌生原因二MySQL InnoDB引擎在事务和并发控制上的表现足够稳定。如果是纯个人学习项目用SQLite会更省事但考虑到这是面向多用户并发写入的企业应用SQLite的锁机制会在同时操作同一张表时产生苦难重试体验不好。部署层面后端用GunicornUvicorn双进程组合Gunicorn管理worker进程Uvicorn作为worker内部的ASGI服务器。前端构建后的静态文件交给Nginx处理同时Nginx配置反向代理将/api开头的请求转发到后端服务。这样一套环境我在服务器上实际跑了半年稳定性和并发表现都让我满意。3. 业务建模客户表、跟进表、商机表到底该怎么设计3.1 核心表结构的演进过程表结构设计是我在整个项目里花时间最多的一环。早期我天真地想用一张大宽表存所有客户信息结果字段越来越多查询越来越慢后来不得不拆表重构浪费了不少功夫。最终定稿的核心表如下第一张是用户表。除了常规的id、用户名、密码哈希、姓名、手机号、邮箱之外最关键的是部门ID和角色ID两个外键。部门决定数据范围权限角色决定操作权限这两者配合才能实现上一节提到的分级查看逻辑。第二张是客户表。这是整个系统的数据基石我设计的字段组包含客户名称、统一社会信用代码、所属行业、客户来源、客户等级、负责销售ID、状态字段潜在/跟进中/成交/流失、首次接触时间、最后跟进时间、备注。这里有两个关键设计决策owner_id索引必须建。所有客户查询都带WHERE owner_id条件没有索引的话数据量上万后查询就会成指数级变慢status用的是整数枚举而不是字符串。潜在1、跟进中2、成交3、流失4。字符串在可读性上有优势但在索引大小和查询性能上不如整数团队内部约定好映射关系即可第三张是跟进记录表。每条记录包含记录ID、客户ID、跟进方式电话/微信/上门/邮件、跟进内容摘要、下次跟进时间、创建人ID。我额外加了一个follow_status字段标记这条跟进是否已完成、是否已过期这是后续做自动化提醒的数据基础。第四张是商机表。商机挂在客户之下每个客户可以有多个商机每个商机有预估金额、成交概率、预计结单日期、阶段。这张表的设计价值在于它可以支撑从商机量预判下季度营收这类管理分析而不是单纯记录已发生的订单。3.2 权限模型为什么不搞复杂的RBAC关于权限控制第一版我考虑过引入完整的RBAC模型表设计里要有用户组、角色、菜单、权限四张大表加若干关联表。后来实际操作时发现对于200人以内的CRM系统过度的抽象只会增加维护负担。我最终采用了一个简化方案用户表直接存角色类型超管1、主管2、销售3角色类型决定了两个维度的能力——菜单可见性和数据可见性。这样实现起来只在一个接口里做判断足够满足业务又不会把系统搞得太重。后端在查询客户接口时会根据当前登录用户的角色自动拼接权限条件def get_customer_list(user, page, size): query select(Customer) if user.role ROLE_SALES: # 销售只能看自己的客户 query query.where(Customer.owner_id user.id) elif user.role ROLE_MANAGER: # 主管能看本部门所有下属客户 sub_ids get_subordinate_ids(user.id) query query.where(Customer.owner_id.in_(sub_ids)) # 超管不拼接额外条件直接全量3.3 API接口设计规范与JWT认证后端接口统一遵循RESTful风格核心客户操作对应以下端点操作方法路径客户列表GET/api/v1/customers客户详情GET/api/v1/customers/{id}新建客户POST/api/v1/customers更新客户PUT/api/v1/customers/{id}删除客户DELETE/api/v1/customers/{id}所有接口的响应格式统一为{code: 0, data: ..., message: success}。成功时code0失败时code为错误码。这样前端axios拦截器可以统一处理业务错误不需要每个接口单独判断。认证采用JWT方案登录成功后下发两个tokenaccess_token有效期2小时refresh_token有效期7天。前端把token存在localStorage注意不要存cookie避免CSRF攻击问题。axios请求拦截器自动在header里添加Authorization: Bearer token。响应拦截器检测到401时自动刷新token并重放原请求——这一套机制让用户基本感知不到登录过期体验非常流畅。4. Vue3端到端实现Composition API在CRM后台里的真实运用4.1 setup函数与ref/reactive的正确打开方式Vue3的Composition API在CRM系统里最大的受益点是复杂表单和列表逻辑的模块化。这个系统里有一个典型的场景客户列表页。它包含筛选条件区、表格区、分页器、新建编辑弹窗、批量操作按钮。如果用Options API筛选状态、列表数据、加载状态、弹窗状态会散落在不同区块里互相依赖关系要靠this向上追溯维护起来极其疲惫。用Composition API后我把代码按功能拆成了独立组合式函数// useCustomerList.js import { ref, onMounted } from vue import { getCustomerList, deleteCustomer } from /api/customer export function useCustomerList() { const list ref([]) const total ref(0) const loading ref(false) const queryParams ref({ page: 1, pageSize: 20, keyword: , status: null, source: null }) async function loadList() { loading.value true try { const { data } await getCustomerList(queryParams.value) list.value data.items total.value data.total } finally { loading.value false } } function handleSearch() { queryParams.value.page 1 loadList() } onMounted(loadList) return { list, total, loading, queryParams, loadList, handleSearch } }组件里调用时只需要拿到这些返回的响应式变量和方法// CustomerListView.vue setup() { const { list, total, loading, queryParams, loadList, handleSearch } useCustomerList() const { dialogVisible, currentRow, openEdit, saveCustomer } useCustomerEdit(loadList) return { list, total, loading, queryParams, handleSearch, dialogVisible, currentRow, openEdit, saveCustomer } }这样做的好处太大了。useCustomerList管列表useCustomerEdit管弹窗两者互不干扰抽出来还能在别的页面复用。如果我想要在一个图表页面也展示客户统计直接调用同一个useCustomerList就行不需要从零开始。关于ref和reactive的选择我的经验是单个值时用ref对象嵌套层级多时用reactive。但需要注意reactive的深层响应式在某些极端场景会有坑比如解构对象时丢失响应式。为了避免这个坑我在写组合函数时一律用ref来声明变量包括对象类型也直接ref({})。虽然访问时需要.value稍微多打几个字但心里踏实不会有解构丢响应式的隐患。4.2 动态路由与权限控制的完整实现实现不同角色看到不同菜单的核心是动态路由。用户在登录成功后后端会返回当前用户的角色信息和一个权限标识数组前端根据这些信息动态生成可访问的路由表。我这里实际采用的是前端动态注册路由方案。路由表分成两部分公共路由登录页、404页和业务路由客户管理、跟进管理、商机管理、数据看板、系统设置。业务路由不静态注册而是在用户登录后根据权限标识动态过滤并router.addRoute逐个注册。权限标识的具体设计采用的按钮级控制比如系统管理员的权限数组里包含customer:create、customer:delete普通销售只有customer:view和customer:update。前端定义了一个全局自定义指令v-permission用在按钮上// main.js app.directive(permission, { mounted(el, binding) { const required binding.value const hasPermission userStore.permissions.includes(required) if (!hasPermission) { el.parentNode?.removeChild(el) } } })页面按钮这样写就行el-button v-permissioncustomer:delete typedanger clickhandleDelete(row) 删除 /el-button这套方案的实现成本不高但效果立竿见影——销售界面干净清爽不会看到用不上的按钮老板界面则能看到所有操作入口。4.3 Element Plus表格与表单校验的实战细节CRM系统80%的界面就是表格和弹窗表单。Element Plus的el-table在渲染大量数据时如果用默认配置会有一点性能问题数据超过500条时页面滚动就开始有明显的卡顿感。我当时最有效的优化手段有三个。第一开启虚拟滚动。如果Element Plus版本较新直接在el-table上增加el-table-virtual相关支持或者退一步用el-table的lazy模式配合el-table-column的typeexpand。第二减少不必要列的嵌套表格列能拆平就拆平。第三将分页拉到数组长度后一次只渲染20条记录——这是最简单也最有效的优化。弹窗表单我会统一封装一个CustomerFormDialog.vue组件内部用el-form加校验规则const rules { company_name: [ { required: true, message: 请输入公司名称, trigger: blur }, { min: 2, max: 50, message: 长度在2到50个字符之间, trigger: blur } ], phone: [ { pattern: /^1[3-9]\d{9}$/, message: 手机号格式不正确, trigger: blur } ], industry: [{ required: true, message: 请选择所属行业, trigger: change }] }有一点踩坑提醒el-form的校验在动态循环渲染多个表单时容易出问题。比如客户联系人列表是动态增删的每个联系人都是一个独立表单字段组这时候校验规则的prop必须带索引像contacts[0].name写法稍有偏差校验就会静默失效且很难排查。5. 前后端联调中的实际坑CORS、时间格式化与刷新Token5.1 CORS跨域问题开发环境与生产环境的处理差异前后端分离项目绕不开CORS。开发环境Vite默认跑在5173端口后端FastAPI跑在8000端口浏览器直接请求会被CORS策略拦截。后端需要配置允许跨域from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173], allow_credentialsTrue, allow_methods[*], allow_headers[*], )这里有个我踩过的坑生产环境如果前后端同域就不应该再用CORS中间件但很多人图省事把allow_origins设置成[*]这在实际生产环境会有安全隐患。更合理的做法是使用Nginx把Vue打包后的静态文件和API请求放在同一个域名下通过路径区分——前端访问/走静态文件访问/api走反向代理这样浏览器请求属于同源天然没有跨域问题。5.2 时间字段的序列化格式CRM系统里时间字段非常多跟进时间、下次跟进时间、创建时间、成交时间。前端展示时如果不统一格式会出现两种尴尬——一是后端返回的是ISO格式如2026-03-15T10:30:00Z前端Element Plus表格里直接显示这个字符串非常不友好二是时区问题后端存储的是UTC时间前端在本地时区展示时如果不转时差用户的计划时间会整体偏移。我的处理是在后端统一返回时间戳前端用dayjs格式化展示import dayjs from dayjs function formatDate(timestamp) { if (!timestamp) return - return dayjs(timestamp).format(YYYY-MM-DD HH:mm) }统计报表按周按月聚合时时间段的处理也统一交给后端。比如本周新增客户这个指标后端根据当前日期的星期一做起始点计算再返回统计数量。这样前端不需要关心周一是一周的起点还是周日是一周的起点统一由业务规则决定。5.3 Token过期静默刷新机制的完整闭环前面提到JWT token的刷新逻辑这是联调中最容易出问题的地方。具体实现上我在axios响应拦截器里做了一个判断当捕获到401错误时service.interceptors.response.use( (response) response, async (error) { const originalRequest error.config if (error.response?.status 401 !originalRequest._retry) { originalRequest._retry true try { const { data } await axios.post(/api/v1/auth/refresh, { refresh_token: localStorage.getItem(refresh_token) }) localStorage.setItem(access_token, data.access_token) originalRequest.headers.Authorization Bearer ${data.access_token} return service(originalRequest) } catch (refreshError) { // refresh失败跳转登录页 router.push(/login) return Promise.reject(refreshError) } } return Promise.reject(error) } )有个细节容易被忽略originalRequest._retry标志位必须加上否则当refresh接口本身返回401时拦截器会进入无限循环重试前端控制台会刷出大量报错而且用户会看到界面卡死。我第一版就踩了这个坑后来才加上重试标志位。6. 让系统真正能用起来的隐藏设计6.1 跟进提醒的实现逻辑CRM系统只要上线使用最大的流失风险就是销售忘记跟进客户。我实现了一套自动提醒机制核心原理其实很简单数据库里存了每条客户的next_follow_time后台定时任务每天扫描两次找出今天需要跟进的客户列表然后通过企业微信/Webhook发送提醒消息给对应销售。Python端用APScheduler实现定时任务非常方便from apscheduler.schedulers.asyncio import AsyncIOScheduler scheduler AsyncIOScheduler() async def check_follow_up_reminders(): now datetime.now() tomorrow now timedelta(days1) customers session.execute( select(Customer).where( Customer.next_follow_time tomorrow, Customer.status.in_([STATUS_FOLLOWING, STATUS_POTENTIAL]) ) ).scalars().all() for customer in customers: send_webhook(customer.owner_webhook, customer) scheduler.add_job(check_follow_up_reminders, cron, hour9, minute0) scheduler.add_job(check_follow_up_reminders, cron, hour14, minute0)这个功能上线后销售忘了跟进的概率大幅降低。管理者甚至可以在看板上看到今日待跟进客户数明天再对比看是否按时处理了整个团队的销售节奏感一下就拉起来了。6.2 数据看板不止是好看的图表很多人做数据看板只是前段放几个ECharts折线图、饼图显得高大上。但实际业务中管理者最关心的其实是几个关键指标线索转化率、平均成交周期、销售人均跟进次数。这些指标如果只是画个图没有下文就失去了价值。我在看板里额外做了一项异常预警列表——自动筛选出近7天零跟进的客户、超过预计结单日期7天未更新的商机、以及跟进次数骤降的销售。这个列表会直接推送给主管他不需要自己分析图表直接看异常列表就能快速介入业务。6.3 数据导入导出从Excel迁移的最后一公里企业从Excel切换CRM最大的阻力就是历史数据录入成本。我在系统里提供了一个导入功能支持Excel模板下载、按照固定格式填充客户数据、校验并批量导入。导入时自动做手机号和公司名称的去重把重复数据标记出来让用户选择跳过或覆盖。这个功能看似不起眼但决定了一个系统能否顺利推行上线。导出方面我实现的是列表搜索后一键导出Excel方便销售做线下拜访前的准备也方便管理者做周报汇报。导出任务使用异步方式数据量大时先生成文件再提示下载避免前端长时间等待。7. 部署上线与性能优化实测后台开发完成后部署上线这套流程也有一些值得记录的经验。服务器配置是2核4G系统是Ubuntu 22.04。前端构建时把Vite的build.target设为es2015兼容性更好。Nginx配置了gzip压缩静态资源首屏加载时间实测从1.8秒降到了0.7秒。后端Uvicorn的worker数我调成了2个。2核CPU上不是worker越多越好因为Python的GIL限制worker多了反而会造成上下文切换的开销。数据库连接池设置为max10因为FastAPI的异步特性和同步数据库连接池之间需要做好桥接我用的是asyncmy连接MySQL避免同步阻塞事件循环。上线之后我做了几个性能压测最关键的接口是客户列表查询。在没有索引的情况下2万条客户数据按销售筛选要2.1秒加上索引后降到80毫秒再配合分页实际用户体验很好。如果有条件建议在关键路由上加上Redis缓存客户数据实时性要求不高可以缓存5分钟大幅减轻数据库压力。另外一定要说一个经验上线前必须做数据备份策略。我用了mysqldump做每日凌晨全量备份另加binlog增量备份同时把备份文件同步到对象存储留档7天。任何一个CRM系统数据都是企业的核心资产数据丢失是事故级别的。8. 项目复盘如果在2026年重新做一遍哪些地方我会换方案复盘整个项目的开发和上线过程有几处今天来看可以做得更好的决策点值得拿出来聊一聊。8.1 如果重来后端会考虑DjangoNested AdminFastAPI很好地完成了API服务的工作但项目后期维护管理后台时我却发现需要一个给运营和客服人员使用的简单后台管理界面。这部分功能如果用Vue3再开发一套投入成本不低。如果一开始就选择DjangoDjango Admin自带的数据管理界面可以零成本覆盖这类低频后台操作团队只需聚焦在销售前台体验上。所以说技术选型没有绝对的好坏关键看你项目里有多少管理维护型功能。我后来在几个纯内部工具项目里就直接切回了Django开发效率确实快很多。8.2 PostgreSQL比MySQL更适合做数据分析如果项目在起步阶段就明确了要做较重的数据分析和漏斗洞察PostgreSQL的窗口函数、JSONB字段类型、以及更强大的索引能力会让开发轻松不少。MySQL 8也支持窗口函数了但某些复杂分析场景下我还是得绕好几步写临时表体验不如PostgreSQL顺滑。8.3 Vue3项目用TypeScript是值得的我当时为了团队上手速度选了JavaScript但项目越写越大之后客户字段、商机阶段、接口响应数据类型越来越复杂没有类型约束导致refactor成本很高。如果重新做我会上TypeScript。Vue3加TS的组合在编辑器里的类型提示非常舒服写代码时很多低级错误在编译阶段就会被拦截。不过也要坦白说如果团队成员之前没写过TS前期学习成本确实会垫高一段时间。这个决策要根据团队现状权衡不是非黑即白的。8.4 前端性能优化可以再激进一点首屏加载我虽然压缩了静态资源但整体JS包还是偏大。后续优化方向是路由级代码分割加高频组件按需加载。Element Plus的按需引入我已经做了但业务组件还是倾向于在一个大组件里打包后续计划把所有弹窗表单提取为动态import进一步拉开首屏速度。最后的经验沉淀做完这个项目我最深的体会是一个管理系统的成败往往不在技术难度而在是否贴合使用者的日常工作流。Excel表也能记客户为什么CRM更好因为CRM把记录后的提醒、统计、分配动作自动化了。开发者在做类似系统时多花时间在跟进提醒、权限边界、数据准确性这些非亮点功能上可能比做一个漂亮的大屏图表更有价值。如果这篇文章帮助到了你我在实际操作中还有一个很小的细节愿意多分享一句客户表里的last_follow_time字段看似只是一个普通的时间戳但凡写了这个字段列表页就能做到最近活跃客户排序——这项功能销售几乎没有不喜欢的因为它直接告诉他们哪些客户值得优先跟进。深耕细节小字段也能发挥大作用。
阅读完成 · 觉得有帮助?
咨询建站