去年帮一位做宠物用品的朋友改造门店管理系统,需求聊到最后变成了一个完整的宠物交易平台。他要的不只是商品上架下架,而是把整个交易链路管起来:宠物档案、寄养预约、买卖订单、客户回访、库存盘点,全部塞进一个后台里。当时手头正好是 Vue 2.6 + Node.js + Element UI 这套组合,项目代号就叫“爱它”。断断续续开发了两个多月,踩了无数坑,也沉淀了不少可复用的经验,这篇博客就把整个项目的设计思路和关键实现拆开来讲清楚。
这个系统做出来之后,最大的价值不是“能发布宠物信息”这么简单,而是把宠物交易里最容易出问题的环节,比如同一只宠物被重复下单、买家付款后卖家不发货、疫苗记录和健康档案缺失,都用业务流程和代码逻辑给约束住了。整套系统分成前台展示和后台管理两大块:前台是宠物展示、详情浏览、在线下单、订单追踪;后台是宠物管理、订单审核、客户管理、数据统计。技术栈锁死在 Vue + Node.js + Element UI,数据库用的 MySQL,部署在腾讯云轻量服务器上。适合正在做前后端分离项目、想了解完整业务闭环怎么落地的同学参考。
1. 从业务梳理到技术落地:宠物交易系统到底需要哪些模块
很多人一拿到“宠物交易管理系统”这个需求,第一反应就是做个宠物信息的增删改查,再加个订单表就完事了。真做了才发现,交易流程的复杂度远超想象。宠物不是普通商品,它有品种、年龄、性别、疫苗状态、健康情况、父母血统这些属性,而且每只宠物往往只有一个库存,卖掉了就没了。这就意味着订单模块必须做到“锁库存”,否则两个人同时下单同一只猫,后台数据就对不上了。
我在设计这个系统时,把业务模块分成了四层。第一层是基础数据层,包含用户表、宠物分类表、宠物信息表。用户表里区分了普通用户和管理员,宠物分类表做了两级分类,比如“猫咪 - 英短”“狗狗 - 金毛”,这样前台筛选时可以按大类缩小范围,再按小类精确定位。宠物信息表是所有业务的核心,字段设计上除了基本名称、价格、图片之外,特别加了status字段来标识宠物的状态:0 表示草稿未上架,1 表示展示中,2 表示已被下单锁定,3 表示已售出。
第二层是交易层,包含购物车表和订单表。购物车表很简单,就是 user_id 加 pet_id 的关联。订单表稍微复杂一点,我把订单状态设计成了待付款、待发货、待收货、已完成、已取消、退款申请、退款完成这几个状态,每个状态之间的流转是有严格限制的。比如待付款状态用户主动取消可以,管理员后台也可以关闭订单,但宠物一旦进入待发货状态,库存就彻底锁死了,不允许再被其他用户看到。
第三层是互动层,包含收藏表、评论表和咨询记录表。收藏表用于前台“心仪宠物”功能,评论区允许用户对已购买的宠物进行评价,咨询记录表则记录买家和卖家之间的沟通信息。第四层是系统管理,包含轮播图表、公告表和操作日志表。轮播图和公告都是后台可配置的,操作日志记录了管理员的关键操作,方便出问题时追溯。
这套模块设计的关键逻辑在于:先摸清业务场景,再设计数据库,最后才是写代码。我的经验是,如果你拿到需求直接敲代码,八成会在开发到一半时发现表结构不够用,回头改数据库非常痛苦。宁可前期多花两天把字段、状态、关联关系理清楚,也不要后期加班补坑。
技术落地方面,前端我用的是 Vue 2.6 + Element UI + Axios + Vue Router + Vuex,后端是 Node.js 的 Express 框架,配合 mysql2 连接数据库。前端工程通过 Vue CLI 4.x 创建,后端手动搭建目录,不引入过多的重型框架,保持逻辑清晰。整个项目采用前后端完全分离的开发模式,前端跑在 8080 端口,后端跑在 3000 端口,通过代理和 CORS 解决跨域问题。
项目根目录结构(前后端分离) . ├── frontend # 前端工程(Vue 2.6) │ ├── public │ ├── src │ │ ├── api # 接口请求模块 │ │ ├── assets # 静态资源 │ │ ├── components # 公共组件 │ │ ├── router # 路由配置 │ │ ├── store # Vuex 状态管理 │ │ ├── views # 页面视图 │ │ ├── App.vue │ │ └── main.js │ ├── package.json │ └── vue.config.js └── backend # 后端工程(Node.js) ├── app.js # 服务入口文件 ├── config # 数据库配置、密钥配置 ├── controller # 控制器层:接收请求、返回响应 ├── middleware # 中间件:JWT验证、上传处理 ├── routes # 路由定义 ├── service # 业务逻辑层:处理核心业务 ├── sql # SQL 初始化脚本 └── utils # 工具函数:日期、加密、响应封装这个结构是我在实际项目中验证过比较清晰的一种。小项目不用搞微服务,也不用分层分得特别细,但至少要把 controller 和 service 分开,否则业务逻辑全堆在路由回调函数里,后期完全没办法维护。
2. 为什么是 Vue + Node.js + Element UI 这套组合:选型时的真实考量
很多同学在技术选型时会犹豫,到底是 Vue 还是 React,后端是用 Node.js 还是 Java Spring Boot,UI 组件库选 Element UI 还是 Ant Design。我当初选这套组合其实有非常具体的理由,不是盲目跟风。
先说 Vue。这个项目有前台展示和后台管理两块,后台管理部分有大量的表单、表格、弹窗、标签页交互,Vue 的双向数据绑定和指令系统在这种场景下非常顺手。特别是 Element UI 的表格组件配合 Vue 的响应式数据,做订单列表、宠物列表这类页面几乎不用自己手写 DOM 操作,数据和视图自动同步。前台展示部分虽然偏静态,但 Vue 的组件化开发方式也能很好地复用宠物卡片组件、分页组件等。
再说 Node.js。这里有一个关键点:团队的技术栈统一。如果前端用 Vue,后端用 Java,那就需要维护两套语言体系和两套部署流程。Node.js 让整个项目只需要一种语言,前端工程师也能轻松走后端逻辑,沟通成本低很多。更重要的是,宠物交易系统属于中小型业务系统,QPS 峰值不会特别高,Node.js 的异步 I/O 和事件循环机制完全够用。Express 作为最经典的 Node.js Web 框架,中间件生态丰富,上手门槛低,配 mysql2 操作 MySQL 数据库非常稳定。
Element UI 这套组件库,在后台管理系统领域的统治地位不是没有道理的。它的表格组件支持自定义列、多级表头、排序、筛选,表单组件自带校验规则,弹窗、消息提示、确认框一应俱全。我只需要调整配置项和样式变量,就能快速搭出一个专业感很强的后台界面。和 Ant Design 相比,Element UI 的 API 设计更符合 Vue 的思维习惯,组件属性都是声明式的,学起来不费劲。
这套组合的局限性也要说清楚。如果是高并发、大数据量的系统,Node.js 单线程的弱点就会暴露;如果项目对 UI 定制化程度要求极高,Element UI 默认样式反而会成为束缚。但对一个宠物交易管理系统来说,这个选型的性价比是最高的。我见过太多团队拿 Spring Cloud 那套做这种小项目,最后维护成本比开发成本还高。
技术选型对比表格 技术维度 Vue + Node.js + Element UI Java Spring Boot + Vue React + Express 开发语言 前后端统一 JavaScript/Node.js 前端 JavaScript,后端 Java 前后端统一 JavaScript 学习曲线 低中 高(Java 体系庞大) 中 UI 组件生态 Element UI 极适合后台管理系统 与 Vue 配合也常用 Element UI Ant Design 优秀,但风格偏复杂 部署成本 轻量,单台服务器 PM2 即可 重,需要 JDK 环境、打包配置 轻量,同 Node.js 中小型业务适合度 高 一般(资源浪费) 中高 维护难度 中等 较高(两套语言体系) 中等3. 数据库与接口层设计:先把业务的地基打牢
数据库设计是我在整个项目中最重视的部分。一个交易系统,数据一致性是最基本的底线。宠物交易系统涉及的实体有用户、宠物、订单、购物车、收藏、评论、公告等,下面是我认为最有借鉴意义的核心表设计。
用户表是最基础的表,我设计了id, username, password, nickname, avatar, phone, email, role, status, create_time这几个字段。密码字段需要注意,绝不能明文存储,我用的是 bcrypt 加密。role字段区分用户角色,0 表示普通用户,1 表示管理员,后续如果需要商家角色,可以扩展为 2。status字段表示账号是否被封禁,封禁用户在前台登录时会直接被拦截。
宠物信息表是整个系统的核心。字段包括品种分类、名称、描述、价格、图片、性别、年龄、体重、是否绝育、疫苗状态、健康状态、所在地,以及最重要的seller_id(卖家用户ID)和status(宠物状态)。为了让每只宠物都有独立展示页面,我增加了pet_no字段作为宠物编号,格式类似PET20250101001,这个编号在订单详情和客服沟通中非常有用。
订单表的设计花了我最多心思。一个订单必须记录订单编号、下单用户、宠物 ID、宠物快照、交易金额、订单状态、收货人信息、物流单号、下单时间、支付时间、发货时间、完成时间。特别注意“宠物快照”这个设计:因为宠物信息可能会被下架或修改,但订单里必须保留下单那一刻的宠物名称和价格,否则后续发生纠纷时没有依据。
订单状态流转我用了一套严格的枚举值来控制:
| 状态值 | 含义 | 可操作动作 |
|---|---|---|
| 0 | 待付款 | 用户支付或取消订单 |
| 1 | 待发货 | 管理员发货或关闭订单 |
| 2 | 待收货 | 用户确认收货或申请退款 |
| 3 | 已完成 | 用户可评价,流程结束 |
| 4 | 已取消 | 流程结束 |
| 5 | 退款中 | 管理员同意退款,原路退回 |
| 6 | 退款完成 | 流程结束 |
事务处理上还有一个核心点:创建订单时一定要开启数据库事务。订单表插入记录和宠物表更新状态这两个操作必须同时成功或同时失败。如果只插入订单但忘记更新宠物状态为 2,就会造成同一只宠物被重复下单。我在service层用connection.beginTransaction()和connection.commit()包裹了这两个操作,并在异常时rollback(),确保数据一致性。
接口层设计遵循 RESTful 风格,核心接口包括:
POST /api/user/register 用户注册 POST /api/user/login 登录(JWT 签发) GET /api/pet/list 宠物列表(分页 + 条件筛选) GET /api/pet/detail?id=xx 宠物详情 POST /api/pet/add 发布宠物(管理员) PUT /api/pet/update 更新宠物信息 DELETE /api/pet/delete?id=xx 删除宠物 POST /api/cart/add 加入购物车 GET /api/cart/list 购物车列表 POST /api/order/create 创建订单 GET /api/order/list 订单列表(分页 + 状态筛选) PUT /api/order/status 更新订单状态 POST /api/upload 图片/视频上传 GET /api/comment/list 评论列表 POST /api/comment/add 发表评论所有接口统一返回结构,这非常重要。我在utils/response.js里封装了一个标准响应函数,格式是{ code: 200, message: 'success', data: {} },前端 Axios 拦截器统一处理。如果后端每个接口返回格式都不一样,前端就要写一堆判断逻辑,纯属自找麻烦。
4. 前端核心业务模块的实现细节:从后台框架到关键页面
前端部分是重头戏,整个后台管理系统加上前台展示页面加起来有二三十个 Vue 组件。我挑几个有代表性的模块讲实现思路。
后台整体布局用的是典型的侧边栏加顶栏结构。侧边栏菜单根据权限动态生成,管理员登录后能看到完整的菜单树,包括仪表盘、宠物管理、订单管理、用户管理、评论管理、公告管理、系统设置。这块我放弃了 vue-element-admin 那种重型脚手架,而是自己手动搭了一个轻量布局,用 Element UI 的el-container、el-aside、el-header、el-main组合。路由表拆成了静态路由和动态路由两部分,静态路由是登录页、注册页、404 页,动态路由是后台业务页面,登录后根据角色权限用router.addRoutes动态注册。
宠物管理页面是后台使用频率最高的页面,我把它分成了宠物列表和发布宠物两个核心视图。
宠物列表用el-table展示,数据来自分页接口。表格列包含宠物编号、名称、分类、价格、状态、发布时间、操作。宠物状态的展示用el-tag标签区分颜色:展示中是蓝色,已锁定是橙色,已售出是灰色。操作列有编辑、上下架、删除三个按钮,上下架操作会调用接口修改宠物状态。这里有一个细节,删除操作我做了二次确认弹窗,而且删除不是物理删除,而是逻辑删除,修改is_deleted字段为 1,列表查询时默认过滤掉已删除数据,这样万一误删还能恢复。
发布宠物表单是整个系统中最复杂的表单,涉及分类级联选择、图片上传、基础信息填写、健康状态勾选。我用el-form加rules校验,图片上传用el-upload组件,限制文件类型为 jpg/png,大小不超过 5MB,上传成功后拿到返回的 URL 地址存入表单。健康状态用复选框组设计成疫苗已打、已驱虫、已绝育、有健康证明四个选项。这个表单的开发让我意识到,一个好的表单设计不只是字段有多全,而是用户填写时是否顺畅。我把所有必填项都放在前面,选填项放后面,价格和库存这类数据用数字输入框限制范围,从源头减少脏数据。
订单管理页面用el-tabs做状态分类,默认展示全部订单,用户也可以按待付款、待发货、待收货等状态快速筛选。订单列表展示订单号、宠物名称、金额、下单用户、状态、时间、操作。点击操作区的“详情”按钮,会弹出一个el-dialog展示订单完整信息,包括宠物快照、收货地址、物流信息、状态流转时间线。时间线我用了 Element UI 的el-steps组件,把待付款、待发货、待收货、已完成四个节点可视化展示出来,用户一看就知道自己的订单走到哪一步了。
前台页面虽然看着没有后台复杂,但有几个交互点需要打磨。宠物展示页面用卡片列表布局,每张卡片展示宠物图片、名称、价格、疫苗状态标签。点击卡片进入详情页,详情页除了信息展示外,还有收藏按钮、加入购物车按钮、立即购买按钮。立即购买会直接调创建订单接口,确认后跳转到支付页面。支付页是模拟的,因为接入真实微信支付需要商户号,我用了 mock 接口模拟支付流程,页面样式按照真实的微信支付扫码界面设计。
Axios 封装是前端工程化的基础工作。我在src/api/request.js里创建了一个 Axios 实例,设置baseURL为/api,通过 Vue CLI 的代理把请求转发到后端 3000 端口。请求拦截器从 localStorage 取出 token,放到请求头Authorization字段里。响应拦截器统一处理后端返回的code,如果是 200 就直接返回data,如果是 401 就清除登录信息跳回登录页,如果是其他错误就Message.error提示用户。这个封装做一次,整个项目所有请求都受益,不用每个页面重复写错误处理逻辑。
// 前端 axios 封装核心代码(src/api/request.js) import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截:携带 token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => Promise.reject(error)) // 响应拦截:统一处理状态码 service.interceptors.response.use(response => { const res = response.data if (res.code !== 200) { if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => Promise.reject(error)) export default service5. 开发中踩过的坑与修复实录:这些问题是搜索热度最高的实战难题
项目开发过程中遇到的坑非常多,结合搜索热度来看,下面几个问题出现的频率极高,几乎每个 Vue + Node.js 开发者都会碰到。
第一个坑是 Windows 环境下 npm 命令无法执行。很多同学在安装完 Node.js 后,打开 PowerShell 执行npm -v,结果报错“无法加载文件 D:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本”。这个问题的根源是 PowerShell 的执行策略默认是 Restricted,禁止运行任何 .ps1 脚本。npm 本身是个命令行工具,但在 PowerShell 下调用时会走 npm.ps1 这个脚本文件,于是被拦截了。解决办法有两种,我推荐最简单的一种:使用 CMD 命令行工具代替 PowerShell,CMD 不会执行 .ps1 脚本,所以不会报错。如果你坚持用 PowerShell,可以执行Set-ExecutionPolicy RemoteSigned然后输入 Y 确认,将执行策略改为允许本地脚本运行。需要注意,这个修改需要管理员权限,否则也会报错。这个坑看似小,但对新手来说真的能卡一下午。
第二个坑是 Element UI 表格固定列变透明。这个问题非常诡异,现象是设置了fixed="right"的表格操作列,在滚动时偶尔会出现背景透明、文字和按钮叠在下面的数据上,完全没法看。搜索“elementui 报表的固定列有时候会变透明”的同学应该都遇到过。这个问题的本质是 Element UI 的固定列原理:它会克隆一份表格放在右侧,用绝对定位覆盖在原表格上方,当滚动时两个层都要刷新,有时浏览器渲染出现 bug,导致固定列的层级或背景色丢失。我的解决办法是给固定列单独加背景色和层级,在全局样式中覆盖:
.el-table__fixed-right { background-color: #fff; } .el-table__fixed-right::before { background-color: #fff; } .el-table th.el-table__cell { background-color: #fff; }这套样式修复了固定列透明的问题,核心思路是给固定列的容器和表头强制加上不透明的背景色,阻止下方内容透出来。如果是深色主题的后台,把#fff换成对应的背景色即可。
第三个坑是 Node.js 环境变量配置。很多同学明明安装了 Node.js,在 CMD 里却提示不是内部或外部命令。这是因为安装时没有勾选“Add to PATH”选项,或者安装路径中包含中文/空格导致系统无法正确解析。解决方法有两种:重新安装时勾选加入 PATH;或者手动配置环境变量,在系统变量的 Path 中添加 Node.js 的安装目录,比如D:\dev\nodejs\。配置完后重新打开命令行窗口,执行node -v验证。
第四个坑是前后端联调时的跨域问题。前端跑在 8080,后端跑在 3000,前端直接请求后端接口会报 CORS 错误。我在早期开发时图省事直接安装cors插件并开启全部跨域:
const cors = require('cors') app.use(cors())但这种方式在生产环境非常危险,等于允许任何源访问你的接口,容易引来恶意请求。后来我改成了白名单模式:
const cors = require('cors') const whitelist = ['http://localhost:8080', 'http://127.0.0.1:8080', 'https://admin.xxx.com'] app.use(cors({ origin: function (origin, callback) { if (!origin || whitelist.indexOf(origin) !== -1) { callback(null, true) } else { callback(new Error('Not allowed by CORS')) } }, credentials: true }))开发环境实际上也可以完全不用 CORS 插件,通过 Vue CLI 的 devServer 代理转发即可。我在vue.config.js中配置了代理:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }这样前端请求/api/pet/list会被代理到http://localhost:3000/pet/list,浏览器看到的是同源请求,不会触发跨域限制。同时后端接口统一挂在一个子路由下,代理配置也清晰。生产环境则通过 Nginx 反向代理把/api路径转发到 Node.js 服务,效果一样。
第五个坑和流媒体播放有关。项目中有一个宠物视频展示模块,管理员可以上传宠物的视频介绍,前台页面在线播放。我最初用的video标签直接播放 mp4 格式,但视频文件一多、码率一高,加载就变得很慢。后来了解到 HLS 流媒体协议可以按需加载切片,配合hls.js或video.js在浏览器端播放.m3u8直播流。不过宠物系统里的视频是点播场景,不是直播,所以我最终没有引入完整的 HLS 方案,而是对视频做了压缩转码上传,限制单文件大小不超过 50MB。这里想提醒大家的是,在 Vue 中播放 m3u8 流时要用hls.js库,并通过video标签的src指向.m3u8地址,且必须确保后端接口允许跨域或走代理,否则浏览器会被 CORS 拦截导致黑屏。这个坑在移动端小程序的 web-view 里尤其明显,我调试了很久才发现是服务端没加 CORS 响应头。
6. 安全设计、部署上线和后续优化方向
一个交易系统最怕什么?最怕用户数据泄露和交易数据被篡改。所以安全设计绝对不能马虎。我在这个项目里做了三层安全防护。
第一层是身份认证。用户注册时密码用 bcrypt 加密存储,登录成功后后端签发 JWT Token。Token 的有效期设置为 24 小时,保存在前端 localStorage 中。每次请求时携带 Token,后端通过中间件解析 Token 验证身份。这里需要特别说一下,JWT 的密钥一定不能硬编码在代码里,我是从环境变量中读取的。而且 Token 只能用来验证身份,绝不能把敏感信息直接明文放在 Token 的 payload 里。
第二层是接口权限控制。管理员接口和用户接口要区分开,我在后端中间件里做了角色校验,只有 role 为 1 的管理员才能访问/api/admin/下的接口,普通用户访问直接返回 403。前端虽然隐藏了管理后台的入口,但不能只依赖前端隐藏,后端必须做真正的权限校验,否则别人直接请求接口地址就能绕过。
第三层是数据安全。数据库操作全部使用参数化查询或预处理语句,防止 SQL 注入。文件上传做了类型白名单和大小校验,只允许 jpg、png、mp4 等指定格式,文件名用时间戳加随机数重命名,避免路径穿越攻击。
部署上线我用的是 Nginx 加 PM2 的组合。前端代码执行npm run build打包生成 dist 文件夹,把 dist 文件夹放到服务器上,Nginx 配置 root 指向它。后端代码上传到服务器后,用 PM2 启动app.js,PM2 会作为守护进程管理 Node.js 应用,进程崩溃了会自动重启。Nginx 配置了/api路径的反向代理,转发到本地 3000 端口:
server { listen 80; server_name your-domain.com; root /var/www/aitaidist; index index.html; location / { try_files $uri $uri/ /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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }关于后续的优化方向,我梳理了三个优先级比较高的点:一是引入 Redis 缓存,宠物列表和分类信息属于读多写少的冷数据,用 Redis 缓存可以大幅减少数据库压力;二是把支付功能对接微信支付或支付宝沙箱环境,替换掉现在的模拟支付;三是增加消息推送,订单状态变化时通过 WebSocket 或短信通知用户,提升交易闭环的体验。
这个项目上线后实际运行了几个月,整体稳定性不错,真正帮我朋友把线下零散的单子收拢到了线上统一管理。做这类管理系统,我的个人体会是:技术栈不是越新越好,业务梳理和数据结构设计才是决定成败的关键。现在回头看,开发周期里超过一半的时间其实花在业务分析和踩坑修复上,真正写业务代码的时间并不长。如果你正准备做一个类似的管理系统,建议先把业务流程图和数据表设计画好,再开始写第一行代码,这条路会顺很多。