ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Node.js与Vue3的出租车公司业务管理系统开发实战

2026/9/29 15:52:23 拓冰建站 浏览量
基于Node.js与Vue3的出租车公司业务管理系统开发实战 1. 项目定位与技术栈选型出租车公司的管理痛点在哪里先说结论出租车公司的业务管理网站本质上是一个中小型的多角色信息管理系统它不像电商平台那样有巨大的并发流量也不像金融系统那样对事务一致性要求苛刻但它有一个很典型的特点——角色多、状态多、单据流转链条长。我做这个项目时客户是一家拥有约80台出租车的区域性出租车公司。他们的业务现状是这样的司机每天交班时要到公司填写纸质交接单调度员用Excel表格记录车辆派单情况财务月底对账要翻一个月的纸质票据。整个流程不是跑不起来而是效率极低、对账困难、数据无法沉淀。比如某辆车这个月出了几次事故、哪个司机连续三个月营收下滑、哪笔订单的油补该发没发这些数据全都散落在纸质单据和个人记忆里。所以当我们要用技术手段去解决这类问题时第一步不是选框架而是先梳理清楚系统要服务的角色和业务逻辑。这个网站需要解决的核心问题有三个车辆全生命周期管理从车辆信息登记、年检提醒、保险到期、维修记录到报废状态都需要线上化。司机与订单的流转管理司机排班、派单、交班、营收结算订单状态要从待接单流转到进行中再到已完成最后进入结算池。多角色协同管理员、调度员、财务、司机四类角色看到的页面和能执行的操作完全不同。基于以上需求我选择了Node.js Express Vue 3 MySQL这一套技术栈。为什么不用更流行的Java Spring Boot原因很实际这个项目的核心业务逻辑是状态流转和CRUD操作Node.js的异步非阻塞模型处理这类IO密集型任务效率非常高而且Express生态成熟、上手快。对于一家出租车公司来说系统后期大概率需要对接GPS定位、计价器数据、支付网关等外部服务Node.js在这类轻量级接口对接上开发效率极高。Vue这边我选的是Vue 3 Element Plus组合。Vue的响应式数据和组件化开发特别适合管理后台这种表格多、表单多、联动多的场景。Element Plus提供开箱即用的表格、弹窗、表单校验组件能省下大量UI开发时间。有人会说管理后台用React也行但对我来说Vue的模板语法和单文件组件结构在快速迭代业务页面时体验更顺滑。小提示如果你的系统未来可能扩展到大型分布式架构可以考虑Spring Boot但如果像我一样目标是用最快的速度交付一个能真正用起来的系统Node.js Vue绝对是一个性价比极高的选择。技术栈确定后整个项目的模块划分也就清晰了。我把系统拆成三个主要部分司机管理模块、车辆管理模块、订单与结算模块外加系统管理用户、角色、权限。下面我会从环境搭建开始逐一分享每个环节的实现细节和踩坑记录。2. 开发环境从零踩坑Node.js安装与npm.ps1执行策略问题这个章节我把它放在最前面因为很多新手包括当年的我在环境配置阶段就被卡住了后面所有计划全部泡汤。先说说Node.js安装的一个容易忽略的细节安装路径。默认情况下Windows下的Node.js安装包会装到C:\Program Files\nodejs\或C:\Program Files (x86)\nodejs\而Program Files目录是有系统权限限制的。如果后续要通过npm全局安装包比如npm install -g pm2很容易因为权限不足导致安装失败。所以我的建议是安装时手动把路径改成D:\nodejs\或C:\nodejs\这样没有空格和系统限制的目录。这个细节在官方文档里不会写但能在后面省掉很多麻烦。安装完Node.js之后紧接着就会遇到热搜词里反复出现的那条经典报错npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本这条报错的本质是PowerShell的执行策略Execution Policy限制了.ps1脚本的运行。npm本身是一个命令行工具但它有一个PowerShell脚本包装器npm.ps1当你直接用PowerShell执行npm命令时如果系统执行策略是Restricted就会拒绝运行。解决方案有两种以管理员身份打开PowerShell执行Set-ExecutionPolicy RemoteSigned然后输入Y确认。这个策略表示本地脚本可运行远程脚本必须有签名是生产环境相对安全的配置。或者干脆在项目里改用cmd终端来执行npm命令绕开PowerShell的限制。我个人的做法是第一种因为后续还有很多依赖需要全局安装Restricted状态下连npm run dev都可能被拦。要注意Set-ExecutionPolicy修改的是当前用户的执行策略如果你在Windows Server上部署还需要检查服务账户是否有权限。环境变量配置是另一个常见坑。Node.js安装包一般会自动把node.exe所在的目录写入PATH但npm全局安装包的路径通常是%APPDATA%\npm有时不会自动加入。如果你执行npm install -g vite之后命令提示符告诉你vite不是内部或外部命令大概率就是这个原因。解决办法打开系统属性→环境变量在PATH里添加上C:\Users\你的用户名\AppData\Roaming\npm然后重启终端。再说说npm镜像源的问题。国内直连npm官方源的速度时好时坏尤其是安装electron这种动辄几百MB的包时很容易超时。我习惯在项目根目录创建一个.npmrc文件写上registryhttps://registry.npmmirror.com electron_mirrorhttps://npmmirror.com/mirrors/electron/第一行把npm镜像切换到国内源第二行单独指定electron二进制文件的镜像。这样既不会全局污染其他人的环境又能显著提升安装速度。实测下来同一个项目依赖的完整安装时间能从15分钟缩短到3分钟左右。最后检查一下安装结果node -v # 输出 v18.18.0 之类的LTS版本号 npm -v # 输出 9.x.x到这里环境就算准备好了。接下来进入重点项目开发阶段。如果你在Windows上用的是Visual Studio Code建议把默认终端改成Git Bash或Command Prompt可以有效避开PowerShell执行策略带来的干扰。3. 后端API设计围绕车辆、司机、订单、结算四条主线的数据建模出租车公司业务管理系统的后端核心工作就是设计数据表和对应的API接口。我在这里分享两个原则一是表结构设计要贴近实际业务状态二是接口要遵循RESTful规范但不要过度设计。3.1 数据库设计六大核心表整个系统的数据模型我规划了六张表用户表sys_user、角色表sys_role、司机表driver、车辆表vehicle、订单表order_info和结算表settlement。其中订单表是整个系统的心脏它的字段设计直接决定了后续功能的复杂度。以订单表为例核心字段包括CREATE TABLE order_info ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, driver_id bigint(20) DEFAULT NULL COMMENT 接单司机ID, vehicle_id bigint(20) DEFAULT NULL COMMENT 车辆ID, customer_name varchar(50) DEFAULT NULL COMMENT 乘客姓名, start_address varchar(255) DEFAULT NULL COMMENT 起点, end_address varchar(255) DEFAULT NULL COMMENT 终点, order_status tinyint(4) DEFAULT 0 COMMENT 0待接单 1进行中 2已完成 3已取消, amount decimal(10,2) DEFAULT 0.00 COMMENT 订单金额, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_driver_id (driver_id), KEY idx_order_status (order_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有一个设计上的小心思order_status用tinyint类型而不是直接存中文字符串这样既节省存储空间又方便程序里做状态枚举判断。而idx_driver_id和idx_order_status这两个索引是必要的因为日常查询基本都围绕某个司机跑了哪些单和当前有哪几单进行中展开。3.2 Express路由与业务分层后端框架我选择Express 4.x虽然Express 5已经发布但4.x的中间件生态更稳定相关踩坑资料也多。项目目录结构按功能模块拆分而不是按技术层拆分server/ ├── routes/ # 路由定义 │ ├── auth.routes.js │ ├── driver.routes.js │ ├── vehicle.routes.js │ └── order.routes.js ├── controllers/ # 业务逻辑 ├── models/ # 数据库操作 ├── middleware/ # JWT鉴权、日志等中间件 └── app.js # 入口文件为什么把路由和业务逻辑分开因为如果直接把SQL写在路由回调里不到500行代码就会变得无法维护。分开的好处是路由只负责请求分发和参数校验controller处理具体业务逻辑models封装SQL操作。这样每一层的职责单一后期加功能或者修bug时能快速定位。3.3 JWT鉴权与多角色权限控制出租车管理系统有四类角色管理员、调度员、财务、司机。不同角色能访问的接口完全不同比如财务能看到结算信息司机只能看到自己的订单。我用JWTJSON Web Token 自定义中间件来实现鉴权。登录成功后后端返回一个包含角色信息的JWT Tokenconst jwt require(jsonwebtoken); // 登录成功后生成token const token jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: 2h } );前端每次请求在Authorization头带上这个token后端用一个全局中间件来解析和校验// middleware/auth.js const authMiddleware (req, res, next) { const token req.headers.authorization?.split( )[1]; if (!token) return res.status(401).json({ message: 未登录 }); try { const decoded jwt.verify(token, process.env.JWT_SECRET); req.user decoded; next(); } catch (err) { return res.status(401).json({ message: Token失效请重新登录 }); } };对于需要仅管理员可操作的接口再包一层requireRole(admin)中间件即可。这样实现的权限控制足够清晰也容易扩展。3.4 订单状态流转的业务逻辑订单模块的后端逻辑是这个系统最复杂的部分难点不在于增删改查而在于状态流转的合法性判断。比如一个已完成的订单不能被重新派单一个进行中的订单不能直接取消。如果在业务逻辑层不加以控制前端误操作或接口被恶意调用就会产生脏数据。我的做法是在controller里定义一个状态流转表const ORDER_STATUS { PENDING: 0, // 待接单 PROCESSING: 1, // 进行中 COMPLETED: 2, // 已完成 CANCELLED: 3 // 已取消 }; const TRANSITIONS { [ORDER_STATUS.PENDING]: [ORDER_STATUS.PROCESSING, ORDER_STATUS.CANCELLED], [ORDER_STATUS.PROCESSING]: [ORDER_STATUS.COMPLETED, ORDER_STATUS.CANCELLED], [ORDER_STATUS.COMPLETED]: [], [ORDER_STATUS.CANCELLED]: [] }; // 每次更新状态前先校验 const canTransition (current, next) TRANSITIONS[current]?.includes(next);这样写的好处是状态与状态之间的合法跳转关系一目了然即使未来新增一个退款中状态也只需要在表里加一条记录。3.5 接口响应格式统一后端接口的返回值格式如果不统一前端联调时就会很痛苦。我定义了一个通用的响应包装函数// utils/response.js const success (res, data, message 操作成功) { res.status(200).json({ code: 200, message, data }); }; const error (res, message, code 400) { res.status(code).json({ code, message, data: null }); };所有controller统一调用这两个函数前端axios拦截器也就只需要处理一个固定的数据格式。4. 前端Vue业务页面路由权限、状态管理与表单交互的实现细节前端这部分是工作量最大的环节因为管理后台的页面数量多而且交互密集。我以Vue 3 Vite Pinia Element Plus为技术基础这里重点聊聊几个容易被忽视的实现细节。4.1 动态路由与路由守卫普通的Vue项目路由表是写死的但出租车管理系统必须根据登录用户的角色动态生成菜单和路由。比如司机账号看不到车辆管理菜单调度员账号看不到财务结算菜单。实现思路分三步登录成功后后端返回当前用户的角色信息前端根据角色从静态路由表里筛选出有权限的路由通过router.addRoute()动态注册在全局路由守卫router.beforeEach里判断如果用户已登录但动态路由还没注册就先注册再放行否则直接放行。核心代码如下// router/index.js const dynamicRoutesMap { admin: [...], dispatcher: [...], finance: [...], driver: [...] }; router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); return; } if (token !store.rolesLoaded) { // 动态注册路由 const roles store.userInfo.roles; const routes roles.flatMap(role dynamicRoutesMap[role] || []); routes.forEach(route router.addRoute(route)); store.setRolesLoaded(true); next({ ...to, replace: true }); } else { next(); } });这里的{ ...to, replace: true }是关键小技巧因为addRoute后当前路由表还不完整直接next()可能造成循环重定向或路由不匹配的问题用这种方式重新触发一次导航就能解决。4.2 Pinia状态管理用户信息与系统配置我选用Pinia作为状态管理库它比Vuex更轻量TypeScript支持也更好。在这个项目里全局状态我拆成了三个storeuserStore用户信息、Token、角色、orderStore订单列表的选中状态、筛选条件、appStore侧边栏折叠状态、全屏加载动画。实际开发中很容易犯的一个错误是把所有接口数据都塞进store里。我后来总结出一个经验只有那些需要跨组件共享的数据才值得放进store。比如当前登录司机的本月营收这个数据多个页面首页统计、司机详情、结算列表都要用到就应该放进store。而订单详情页的数据只在自身页面展示直接通过接口获取、用组件本地变量保存即可没必要全局共享反而让store变得臃肿。4.3 Element Plus表格组件的二次封装管理后台最常见的页面模式是搜索区 表格区 分页区。如果每个列表页都重复写一遍表格和分页逻辑代码冗余会非常严重。我封装了一个BusinessTable组件它接收列配置、接口地址、查询参数内部统一处理加载状态、分页变更、数据刷新。!-- components/BusinessTable.vue 简化版 -- template div classbusiness-table el-table :datatableData v-loadingloading el-table-column v-forcol in columns :keycol.prop :propcol.prop :labelcol.label :widthcol.width / /el-table el-pagination :current-pagecurrentPage :page-sizepageSize :totaltotal current-changehandlePageChange / /div /template实际在封装时要注意一个细节el-table-column中如果使用了插槽比如操作列需要自定义按钮需要在列配置里增加一个type: slot的标记然后在组件内部动态渲染对应的具名插槽。这样才能兼顾80%的常规场景和20%的定制场景。4.4 订单列表的筛选与多条件联动司机和管理员最常用的功能是订单列表的筛选筛选条件通常包括日期范围、订单状态、司机姓名。这里有一个前后端配合的细节——筛选参数如果直接拼在URL里刷新页面后状态会丢失。我的解决方法是把筛选条件存入Pinia的orderStore刷新页面后从store恢复。具体来说订单页面的onMounted里先检查store里有没有上次的查询条件如果有自动填充到搜索表单并重新发起请求如果没有使用默认条件比如最近7天。为了让列表的查看详情操作更像真实业务系统我做了两个内容非常详细的详情抽屉一个是展示乘客信息、起终点、费用明细、司机信息的订单详情另一个是展示车辆维修记录的车辆详情。用户点击某行记录时从右侧滑出详情面板而不是跳转到新页面。这种交互在管理后台里比跳转页面更高效值得推荐。4.5 Vite开发环境解决跨域问题开发阶段最挠头的问题是跨域尤其是Vue跑在5173端口后端Express跑在3000端口浏览器直接请求会有CORS限制。我没有在后端专门配置CORS中间件而是在Vite的配置文件里设置了代理// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } });这样一来前端请求的URL写/api/driversVite在开发阶段会自动转发到http://localhost:3000/drivers。而生产环境部署时我用Nginx的反向代理把/api前缀再次转发到Node服务。这样设计的好处是前端代码里所有请求路径都带/api前缀一份代码在开发和生产环境都能正常工作不需要修改任何环境变量。5. 前后端联调与异常处理从接口约定到生产环境验证前后端联调是项目开发中耗时最长、最容易出问题的阶段。我总结了几个最容易踩的坑以及对应的解决方案。5.1 axios请求封装与统一错误处理我封装了一个request.js作为全局请求入口核心逻辑包括请求拦截器里自动从localStorage读取Token并写入Authorization头响应拦截器里统一处理后端返回的code字段如果code不是200弹出错误提示用的是Element Plus的ElMessage.error检测HTTP状态码401表示Token过期自动清除本地登录状态并跳转登录页。这里的核心问题是Token过期后的处理方式要仔细设计。如果只是简单跳转到登录页用户在填写长表单时突然被踢出去体验极差。我的方案是当接口返回401时先不立即跳转而是弹出一个登录已过期请重新登录的确认框用户点击确认后才跳转登录页并把当前页面的路由记录下来登录成功后自动跳回原页面。这个细节虽然简单但用户的满意度会大幅提升。另外有一个特别容易忽略的地方文件下载接口的响应格式与JSON接口不同。如果某个接口返回的是Excel文件流常规的axios拦截器会尝试解析JSON导致下载失败。我处理的方式是对下载类的请求关闭统一拦截单独在组件里处理response.data为Blob对象后用URL.createObjectURL生成临时下载链接。5.2 跨域问题在生产环境的表现前面提到开发环境用Vite代理解决跨域但生产环境如果也忘了处理会出现一种更隐蔽的报错请求能到达服务器服务器也能正常返回但浏览器拦截了响应控制台报CORS错误。这种问题排查起来非常费时间因为它不会显示请求失败而是显示CORS policy: No Access-Control-Allow-Origin header。在生产环境我的处理方案是在Nginx配置里加代理转发location /api/ { proxy_pass http://127.0.0.1:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样浏览器请求的是Nginx的地址相当于同源请求Nginx再把请求转发给Node.js服务完全规避了跨域问题。5.3 表格数据刷新与缓存一致性在管理后台里用户常常会遇到这样的操作流在A列表页删除了某条记录然后在B列表页刷新却发现数据仍然是旧的。如果每个列表页在onMounted时都从接口重新取数理论上不会有这个问题但实际中为了性能我给部分列表页加了keep-alive缓存。这就导致删除操作后被缓存的页面不会自动更新。我的解决方法是在删除、编辑操作成功后触发一个全局事件可以是一段简单的mitt事件总线被缓存的列表页监听这个事件后自动刷新数据。比如// 新增/编辑/删除操作成功后 mitt.emit(table-data-changed, { table: driver }); // 列表页 mitt.on(table-data-changed, (payload) { if (payload.table driver) { fetchDriverList(); } });这样做虽然会在多页面同时监听时多发起几次请求但能保证用户看到的数据始终是最新的业务上的误判会大幅减少。5.4 生产环境验证清单联调完成不代表结束我在正式交付前通常有一套完整的验证清单用无痕浏览器窗口测试未登录状态下的页面跳转是否正常分别用管理员、调度员、财务、司机四个角色登录确认菜单和按钮权限无误在慢网速条件下DevTools里选择Slow 3G检查加载动画和错误提示是否合理提交一次包含非法字符的表单比如在手机号字段填中文确认后端的参数校验能正常返回错误信息。这些验证的点往往是客户真实使用时最容易出问题的位置。一个严谨的交付流程不仅是对用户负责也是给自己省掉大量售后排查的时间。6. 部署上线与日常维护从Windows服务器到进程守护的实践经验系统的部署环境我选了Linux云服务器原因很简单Linux下部署Node.js应用生态更成熟进程守护、日志管理都有现成工具。下面这些经验是我在多次部署后总结出来的。6.1 生产环境Node.js与npm配置在服务器上安装Node.js时不同Linux发行版的包管理器自带的版本往往不是最新的。比如Ubuntu 20.04自带的Node.js是10.x而很多新版本依赖比如Vite 5要求Node.js 18以上。推荐用NodeSource源或者nvm来安装指定版本。我的习惯是装nvm因为后续如果涉及项目升级或同时维护多个项目nvm可以随时切换Node版本curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash nvm install 18.18.0 nvm alias default 18.18.0Node版本确定后再配置npm config set registry指向国内镜像缩短安装时间。6.2 使用PM2守护进程与日志管理Node.js应用的进程守护我用的PM2。很多人直接在服务器上用node app.js启动服务一旦进程崩溃或者服务器重启服务就再也起不来了。PM2可以作为系统服务管理Node进程还自带内存监控和日志轮转。启动命令非常简单pm2 start app.js --name taxi-server --env production几个实用操作pm2 save # 保存当前进程列表开机自启 pm2 startup # 生成开机自启脚本 pm2 logs taxi-server # 查看实时日志 pm2 restart taxi-server # 重启服务我的经验是PM2的日志管理默认是把console.log和错误输出写到同一个文件但如果项目运行时间长了日志文件会变得非常大。建议在pm2 start时加上--merge-logs --log-type json参数或者直接用pm2-logrotate这个扩展来做自动轮转。否则半年后你打开日志目录发现一个文件有几个GB排查问题时翻日志都会卡死。6.3 Nginx反向代理与静态文件托管前端构建产物是纯静态文件可以直接用Nginx托管同时把/api请求反向代理到Node服务。我的Nginx配置核心片段如下server { listen 80; server_name taxi.example.com; # 前端静态文件 root /var/www/taxi/dist; index index.html; # 前端路由history模式必须配置 location / { try_files $uri $uri/ /index.html; } # 后端API代理 location /api/ { proxy_pass http://127.0.0.1:3000/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }这里有一个对Vue应用至关重要的配置try_files $uri $uri/ /index.html。因为Vue Router用了HTML5 History模式URL里没有#号用户在浏览器直接输入http://taxi.example.com/drivers时Nginx要在后端找不到这个路径的情况下把请求转发给index.html让Vue Router接管前端路由。如果没有这行配置刷新任何一个子页面都会报404。6.4 数据库备份策略出租车订单数据是公司的核心资产数据库备份不能马虎。我在服务器上写了一个简单的备份脚本每天凌晨3点执行MySQL导出并保留最近7天的备份文件#!/bin/bash BACKUP_DIR/var/backups/mysql DATE$(date %Y%m%d) mysqldump -u root -p密码 taxi_db ${BACKUP_DIR}/taxi_db_${DATE}.sql find ${BACKUP_DIR} -name taxi_db_*.sql -mtime 7 -exec rm {} \;再用cron加入计划任务0 3 * * * /usr/local/bin/backup_taxi_db.sh /var/log/backup.log 21实际部署中我对这个脚本做了一些优化导出后用gzip压缩再传一份到另一台远程服务器避免服务器本身磁盘损坏导致备份全部丢失。对于出租车公司这种小规模业务数据每天一次增量备份已经足够要求再高一些的话可以改成每6小时一次。6.5 交付后的运维心得系统上线交付后我总结了几条运维心得初期一周最重要的是观察错误日志很多业务逻辑漏洞只有在真实数据量下才会暴露比如某条SQL在几十条数据时跑得飞快但数据量到几千条后因为缺少索引变慢。Node.js服务的--max-old-space-size参数值得留意默认内存上限大约1.4GB如果业务增长很快在PM2启动命令里指定node_args: --max-old-space-size2048可以提前规避内存溢出问题。不要轻易在生产环境执行npm update版本锁定到package-lock.json的状态除非有明确的安全漏洞需要修复。这个项目从需求梳理到正式交付上线前后花了不到两个月。对于出租车公司业务管理网站这类系统技术难度并不算高真正的价值在于你对业务的理解出租车公司要的不是一个炫酷的驾驶舱仪表盘而是司机今天该交多少钱哪台车该年检了这个月总营收是多少这些琐碎问题的快速解答。Node.js和Vue是我用来实现这些答案的工具而它们恰恰是轻盈、高效又足够灵活的组合。如果你也在规划类似的管理系统项目希望这篇分享能帮你少走一些弯路把更多时间花在理清业务逻辑上。