
做后台管理系统我最常被问到的就是客户关系管理CRM到底要不要自己写。市面上的产品要么太贵要么功能堆砌用不上小团队往往只需要“管客户、记跟进、看数据”这三板斧。我自己用 Node.js Vue 完整实现过一套轻量级的客户关系管理系统从零搭建到上线跟进用了不到三周。这篇就把整个设计思路、技术选型、核心代码结构和实际踩坑过程全部摊开讲适合有 Node.js 和 Vue 基础、想独立做一个完整项目的开发者参考。先说结论这套系统核心就三块内容——客户信息管理、跟进记录追踪、销售数据看板。后端用 Express 提供 RESTful API前端用 Vue 3 配合 Element Plus 搭界面数据库用 MySQL 存储。整个项目不依赖复杂框架代码结构清晰能跑通完整的“登录认证 - 数据增删改查 - 报表统计”链路。接下来我会按照实际开发顺序从环境准备到最终部署把每一步的关键细节和容易踩的坑都写清楚。1. 系统整体设计与技术选型思路1.1 为什么是 Node.js Vue 这个组合技术选型这件事很多人会纠结但我的判断标准就两条团队上手成本低、项目迭代速度快。Node.js 的 Express 框架中间件生态非常成熟写接口和做权限控制都很顺手Vue 对中后台场景的支持很友好模板语法简单配合 Element Plus 组件库客户列表、表单弹窗、表格分页这些 CRM 里最高频的界面都能快速搭出来。这个组合没有用 Spring Boot 那套重型的 Java 体系也没有选择 Python 的 Django核心原因是CRM 是典型的中后台 CRUD 系统不需要高并发、不需要分布式Node.js 单线程的事件循环模型处理这类业务绰绰有余。前后端分离后前端只需要关注界面交互后端只需要提供 JSON 接口分工明确后续要加移动端或者小程序也可以直接复用这一套 API。1.2 功能模块划分与数据库表设计客户关系管理系统的核心对象是“客户”所有功能都围绕客户展开。我在设计功能模块时做了四个维度客户信息管理、联系人管理、跟进记录、数据统计。客户表customer是所有业务的主表设计上必须考虑后续扩展。我的字段设计如下CREATE TABLE customer ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 客户名称, phone varchar(20) DEFAULT NULL COMMENT 联系电话, company varchar(200) DEFAULT NULL COMMENT 公司名称, source varchar(50) DEFAULT NULL COMMENT 客户来源, level tinyint(4) DEFAULT 1 COMMENT 客户等级 1-5, status varchar(20) DEFAULT pending COMMENT 状态pending-待跟进following-跟进中closed-已成交, owner_id int(11) DEFAULT NULL COMMENT 负责人ID, remark text COMMENT 备注, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_owner (owner_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;跟进记录表follow_up和客户表是一对多的关系这是 CRM 的核心业务逻辑。客户可以有多条跟进记录每次沟通都是一次跟进记录里需要包含跟进内容、下次跟进时间、跟进人。这个表设计时要特别注意建立索引因为列表页经常要按照客户 ID 查询所有跟进记录。1.3 为什么选择前后端分离而不是服务端渲染我知道很多人会用 Node.js 直接做服务端渲染类似 PHP 或传统 JSP 那种思路。但在这个项目里我明确选择前后端分离理由有三个。第一客户关系管理系统的交互复杂有大量的弹窗、多级联动、实时数据刷新需求SPA 单页应用的体验比页面跳转好太多。第二前后端分离天然支持多端共用以后发一个钉钉小程序或企业微信应用直接调用后端接口就可以不用改动任何业务逻辑。第三Node.js 后端和 Vue 前端可以独立开发独立测试我在实际开发中先定义好接口文档然后前端可以马上开始写页面不用等后端代码写完。2. 开发环境搭建与基础设施配置2.1 Node.js 安装与环境变量配置这个项目一开始就要把开发环境配好这里也是很多新手卡住的第一关。Node.js 我推荐安装 LTS 长期支持版本普通业务项目用最新版完全没必要稳定压倒一切。下载安装包之后安装路径建议不要带空格和中文虽然带了也能用但后续很多工具对路径敏感会平白增加很多排查成本。安装完成后打开命令行工具输入node -v和npm -v确认版本号。如果提示“无法识别 node 命令”说明安装路径没有配置到环境变量里这时需要手动配置右键“我的电脑” - 属性 - 高级系统设置 - 环境变量在系统变量里找到 Path把 Node.js 的安装目录添加进去。2.2 解决 npm.ps1 无法加载脚本的报错这个问题出现的频率非常高几乎每个在 Windows 上装 Node.js 的人都遇到过一次。错误信息是这样的npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本这个报错跟 Node.js 本身没关系纯粹是 Windows 的 PowerShell 执行策略限制。PowerShell 默认禁止运行任何脚本文件而 npm 在 PowerShell 下是通过 npm.ps1 脚本执行的所以就被拦截了。解决办法有两种。第一种是临时性的每次打开 VS Code 终端后先在命令行执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser然后在弹出询问时输入“Y”回车。这样设置之后当前用户就可以运行本地的 PowerShell 脚本。第二种是一劳永逸的办法直接在环境变量配置完成后把 VS Code 的默认终端从 PowerShell 切换到 CMD因为 cmd 执行 npm 走的是 npm.cmd 文件不受 PowerShell 执行策略限制。这个坑我必须单独拉出来说因为别小看这一条网上搜的时候一堆人给答案解决办法却不全。关键是执行策略设置有两个作用域CurrentUser当前用户和 LocalMachine本机全局如果执行 Set-ExecutionPolicy 时提示“不是管理员权限”就要加上-Scope CurrentUser参数。2.3 Vue 项目初始化与依赖安装前端项目我用 Vue 3 组合式 API配合 Vite 作为构建工具这套组合现在的开发体验非常流畅。创建项目的命令npm create vuelatest这个命令会交互式询问你是否需要 TypeScript、路由、状态管理等。做 CRM 这个项目我推荐选择 Vue Router 和 Pinia其余全部选 No保持项目干净。TypeScript 看个人习惯我用 JavaScript 写业务逻辑更顺手如果你的项目需要维护的人多建议选上 TS 增加代码约束。依赖安装的时候要特别注意element-plus的按需自动导入需要额外配置unplugin-vue-components和unplugin-auto-import这两个插件否则整个 Element Plus 的组件库全部打包初始包体积会大得多。我的配置文件是// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })3. 数据库设计与后端接口实现3.1 用户表与客户表的核心设计逻辑除了前面提到的客户表和跟进记录表系统还需要用户表和商机表。用户表存放系统登录账号密码必须用 bcrypt 加密绝对不能明文存储。我做系统时最看重这块安全设计CREATE TABLE sys_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 账号, password_hash varchar(200) NOT NULL COMMENT 密码哈希, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role varchar(20) DEFAULT staff COMMENT 角色admin-管理员staff-普通员工, status tinyint(1) DEFAULT 1 COMMENT 1启用 0禁用, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY idx_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;商机表是为了把销售流程管理起来它是客户表的延展信息内容包括预计成交金额、所处销售阶段、期望成交日期。这个表的设计逻辑很简单但实际业务中最有价值。我把它和客户表分开而不是在客户表里加字段是因为一个客户可以对应多个商机这是一个一对多的关系。3.2 Express 框架与 RESTful API 设计后端接口我用 Express 4 mysql2 sequelize 实现的。ORM 框架的好处是写数据操作时不用拼 SQL 字符串避免注入漏洞同时字段变更时修改模型文件就能自动同步开发效率提升很大。接口设计遵循 RESTful 原则核心几个接口如下POST /api/auth/login登录认证GET /api/customers客户列表支持分页和搜索POST /api/customers新增客户PUT /api/customers/:id修改客户信息DELETE /api/customers/:id删除客户GET /api/customers/:id/followups获取某客户的跟进记录POST /api/followups新增跟进记录GET /api/dashboard/stats数据统计接口的返回格式必须统一前端才能写统一的拦截器处理。我的格式是{ code: 0, message: success, data: {} }后端代码的关键是 app.js 里注册路由和中间件的顺序const express require(express) const cors require(cors) const authRouter require(./routes/auth) const customerRouter require(./routes/customer) const followUpRouter require(./routes/followup) const dashboardRouter require(./routes/dashboard) const { verifyToken } require(./middleware/auth) const app express() app.use(cors()) app.use(express.json()) app.use(/api/auth, authRouter) // 需要登录认证的路由统一挂载 verifyToken 中间件 app.use(/api/customers, verifyToken, customerRouter) app.use(/api/followups, verifyToken, followUpRouter) app.use(/api/dashboard, verifyToken, dashboardRouter) app.listen(3000, () { console.log(CRM API server running on http://localhost:3000) })3.3 JWT 身份认证的完整流程登录认证我选择 JWTJSON Web Token思路是用户登录成功后后端生成一个包含用户 ID、角色、过期时间的 token 返回给前端。前端把 token 存在 localStorage 里之后每次发起 HTTP 请求都在请求头里带上Authorization: Bearer token字段。后端在 verifyToken 中间件里检查这个 token 是否有效如果有效就通过无效就返回 401 让前端跳回登录页。token 的有效期我设置的是 24 小时。很多人的系统做的比较简陋直接设置 7 天甚至 30 天不失效这样安全性堪忧。对于 CRM 这种带着销售数据的系统必须让 token 定期失效用户长时间不操作就该重新登录。生成 token 的核心代码const jwt require(jsonwebtoken) const SECRET_KEY process.env.JWT_SECRET || your-secret-key-change-in-production function generateToken(user) { return jwt.sign( { id: user.id, username: user.username, role: user.role }, SECRET_KEY, { expiresIn: 24h } ) }验证中间件其实就是 jwt.verify 的封装但要注意异常处理逻辑token 过期和 token 非法要能分辨出来给前端返回不同的提示信息。4. 前端 Vue 核心功能实现4.1 路由设计动态路由与权限控制前端路由我用 Vue Router 4页面结构很简单就三块登录页/login、主布局/layout、子页面客户管理、跟进记录、数据看板。这个项目没有做成复杂的动态路由因为用户角色只有管理员和普通员工两种菜单基本一致没必要动态生成路由表。但有一点要处理——路由守卫。没有登录的用户访问任何业务页面都要强制跳转到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })4.2 客户管理页面表格、搜索、分页一次讲清客户列表页是整个 CRM 的核心界面业务逻辑集中在“搜索 分页 增删改查”。这个页面我用的是 Element Plus 的 Table 组件配合 Pagination 组件表格数据通过 axios 接口从后端拉取。分页参数怎么传这个细节一定要设计好。前端传page页码和pageSize每页条数给后端后端返回{ list: [], total: 100 }前端把 total 传给分页组件分页组件切页时重新请求接口。搜索功能则是把搜索关键词作为额外参数传给后端后端用LIKE查询数据库// 前端请求示例 const params { page: currentPage.value, pageSize: pageSize.value, keyword: searchKeyword.value } const { data } await axios.get(/api/customers, { params })这里有两个关键细节新手最容易忽略。第一个是搜索防抖用户边打字边触发搜索会对后端造成大量无效请求我用lodash的debounce方法来延迟 300ms。第二个是删除确认CRM 里的客户数据删了找不回来删除操作一定要弹确认框这个可以用 Element Plus 的ElMessageBox.confirm实现。新增和编辑我用 Dialog 弹窗表单校验用 Element Plus 自带的 Form 规则。客户名称必填、电话格式校验、等级范围 1-5这些规则在rules对象里配置。提交成功后刷新表格并提示成功消息用ElMessage组件。4.3 跟进记录的时间线设计跟进记录这个模块界面我用的是 element-plus 的 Timeline 时间线组件每个跟进节点展示沟通时间、跟进人、跟进内容、下次跟进提醒。这比单纯的表格直观很多销售同事打开客户档案一眼就能看到最近一次跟进的进展。新增跟进的表单里有一个日期选择器选择“下次跟进时间”。后端收到这个日期后会额外生成一条提醒记录。这个功能看似简单实际实现时需要注意时区问题特别是本地开发用的datetime字段和 MySQL 的DATETIME类型交互时可能会产生 8 小时的偏差。解决的办法有两种一是前端提交的时间统一用时间戳传给后端后端再转格式存储二是后端接口接收 ISO 格式字符串时手动指定timezone。我更推荐统一使用时间戳方案简单可靠。4.4 数据看板ECharts 销售统计图表数据看板这个模块是整个系统最提气色的一块。我用 ECharts 数据可视化库做的两个图表一个柱状图展示每个销售人员的客户数量一个饼状图展示客户状态分布待跟进、跟进中、已成交。图表初始化的代码挂在mounted生命周期里从后端获取统计数据后通过echarts.setOption()更新图表数据。这里要注意一个坑组件切换页面时如果mounted里初始化了图表但是切换后又销毁了 DOMECharts 实例就会报错。解决方法是在组件unmounted生命周期里调用chart.dispose()释放实例。统计接口的 SQL 用GROUP BY实现比较简单SELECT status, COUNT(*) as count FROM customer GROUP BY status;5. 前后端联调与项目部署5.1 开发环境跨域问题的解决路径前后端分离开发最头疼的问题就是跨域。因为前端跑在 Vite 的默认端口 5173后端跑在 3000 端口浏览器会拦截跨域请求。解决跨域我有两种常用方案。第一种是后端开启 cors 中间件这个最省事就是代码里app.use(cors())。这个方法能解决跨域但生产环境会有安全隐患所以我只限定开发环境使用。第二种是前端用 Vite 的 Proxy 代理让前端请求的相对路径转发到后端服务器。这种方式在vite.config.js里配置如下server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }这样前端代码里axios.get(/api/customers)就会被代理到http://localhost:3000/api/customers浏览器看到的请求是发到同源地址跨域彻底消失。生产环境部署时Nginx 也做同样的代理配置所以开发用 Proxy生产用 Nginx一脉相承。5.2 生产环境构建与 Nginx 部署整个项目开发完成后前端需要打包成静态文件。执行npm run buildVite 会把所有资源打包到dist文件夹里。这时把dist文件夹里的所有文件上传到云服务器的 Nginx 配置目录里然后做一个反向代理配置server { listen 80; server_name yourdomain.com; root /var/www/crm/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }需要特别注意的是Vue Router 的 history 模式没有 # 号的那种 URL在 Nginx 下刷新页面会 404。解决办法是在 Nginx 配置里加一句location / { try_files $uri $uri/ /index.html; }这样所有找不到的路径都会回退到 index.html由 Vue Router 自己根据 URL 决定渲染哪个页面。5.3 环境变量与敏感配置的管理生产环境部署时端口、数据库地址、JWT 密钥这些都不要硬编码在代码里。Node.js 支持.env文件我用了dotenv这个包把敏感配置放在项目根目录的.env文件里PORT3000 DB_HOSTlocalhost DB_PORT3306 DB_USERcrm_user DB_PASSWORDyourpassword JWT_SECRETrandom-secure-key代码里require(dotenv).config()之后用process.env.PORT这种方式读取配置。注意.env文件绝对不能提交到 Git 仓库里要在.gitignore说明显加上.env一行。6. 典型问题排查实录与避坑指南6.1 npm 安装依赖时的各种报错这个项目开发中遇到最多的还是环境问题。除了前面提到的 npm.ps1 执行策略问题还有几个高频报错。“npm ERR! code ERESOLVE” 这种报错通常是因为依赖树冲突解决办法可以试试npm install --legacy-peer-deps绕过 peer dependency 的检查。网络问题导致的安装超时也很多解决办法是把 npm 镜像切到国内镜像源npm config set registry https://registry.npmmirror.com6.2 接口请求遇到 404 和 401 的问题排查我遇到过几次“明明接口代码正确前端却一直报 404”的情况。排查思路是从浏览器 F12 的 Network 面板看实际的请求地址确认是否有拼写错误或者路径大小写问题。CommonJS 的 Express 路由路径是大小写敏感的/api/customers和/api/Customers是两个完全不同的接口。401 则是认证问题常见原因是 token 过期。排查方法是在浏览器 Application 面板里看 localStorage 里的 token 是否存在、是否已经过期。还有一个很隐蔽的问题前端请求头写错了正确的写法是headers: { Authorization: Bearer token }少了Bearer前缀后端就解析不出来。axios 拦截器是我处理 token 过期统一跳转的关键axios.interceptors.response.use( (response) response.data, (error) { if (error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )6.3 Node.js 服务崩溃和内存泄漏问题开发 Node.js 接口时我还碰到过一次MySQL server has gone away的报错。原因是 MySQL 闲置连接超时默认 8 小时后连接会被服务端断开但 Node.js 的连接池还拿着旧连接。解决办法是配置连接池的waitForConnections和connectionLimit同时定期heartbeat检查连接是否存活。还有一个经验写接口时一定要做好异常捕获。Express 4 的异步错误不会自动传给错误处理中间件我用了一个包装函数asyncHandler把异步函数包装一下保证错误能走到统一错误处理中间件const asyncHandler (fn) (req, res, next) { Promise.resolve(fn(req, res, next)).catch(next) }这样任何接口的异常都会被捕获不会直接导致 Node.js 进程崩溃。生产环境我还用 pm2 守护 Node 进程进程崩溃后自动重启保证服务连续性。6.4 常见问题速查表问题症状解决方案npm.ps1 执行策略报错PowerShell 下无法运行 npmSet-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserNode 命令不存在node -v提示无法识别配置环境变量 Path 指向 Node 安装目录跨域报错浏览器提示 CORS 错误前端 Vite 配置 proxy 代理或后端开启 cors前端刷新 404Nginx 部署刷新页面白屏配置try_files $uri $uri/ /index.html;数据看板图表白屏切换页面后图表报错组件卸载时调用chart.dispose()MySQL 连接丢失隔一段时间请求报连不上数据库配置连接池心跳检测7. 从零到上线我的整体复盘与实用建议这个项目做下来最大的感受是一个管理系统能不能用起来关键不在于技术多新多酷而在于它是否真正贴合实际业务场景。我最初也考虑过用微服务架构、用 Redis 做缓存、用 Socket 做实时通知后来冷静下来砍掉了绝大多数设计因为一个几个人用的小型 CRM 根本不需要这些复杂度。最终线上稳定运行的就是 Express MySQL Vue 3 这个组合简单直白但也确实够用。部署时我建议第一次先用 docker-compose 统一编排 MySQL 和 Node.js 服务Nginx 放宿主机做静态资源服务和反向代理。这样一套环境在本地和云服务器之间迁移非常方便。后续如果要加文件上传、对接企业微信通知、或者扩展销售漏斗分析都是在这一套骨架上面加接口和组件就行不会大改。如果你正在考虑做一个类似的项目我给你的建议是先定好数据结构表关系梳理清楚后开发效率至少提升一倍前端组件直接用现成的 Element Plus 这种成熟组件库别自己造轮子JWT 、bcrypt、axios 拦截器这些安全基础从一开始就做好别等上线了再补。踩过几个坑之后你也会发现所谓全栈项目其实就是把这些基础模块一个个拼扎实每一步都有章可循。