ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue+Node.js多角色演出售票系统开发实战:从权限设计到防超卖

Vue+Node.js多角色演出售票系统开发实战:从权限设计到防超卖 第一次接这种多角色管理系统的时候很多人第一反应是“不就是个增删改查吗”真正动起手来才发现用户、卖家、管理员三个角色叠在一起权限、数据隔离、业务流程全都要重新捋一遍。这个基于vue和nodejs的线下演出售票管理系统恰恰是这类项目的典型代表。如果你正准备做类似的项目或者是拿来当毕业设计、简历项目这篇内容把从业务拆解、技术选型、数据库设计到前后端落地和排坑的完整过程都讲透照着走能省不少瞎琢磨的时间。先说清楚这个系统解决的核心问题。线下演出演唱会、话剧、音乐节、livehouse的售票场景跟电商买普通商品不是一回事。票是有库存上限的一个场次可能分多个票价档位每档对应不同座位区域还存在热门场次秒没、临时加场、退票回流这些情况。这时候买家、卖家、管理员三个角色同时存在谁的权限在哪、数据怎么隔离、售票流程怎么保证不超卖都是需要提前定清楚的东西。vue负责前端页面nodejs负责后端接口一前一后配合起来刚好能用一套js语言把整个项目打穿。1. 项目全貌先把三个角色和业务流程揉碎了分析1.1 这个系统到底解决什么问题线下演出售票管理系统本质上是一个带库存控制的交易平台只不过商品变成了“某场演出的某个价位的票”。跟普通电商比它有几个明显的特点第一商品有时效性和唯一性。演唱会今晚演完票就作废了同一个场次座位不能卖两次。第二库存需要实时扣减。两个人同时抢最后一张票系统只能让一个人成功下单这就是典型的高并发一致性场景。第三角色天然分权。买家只能买票和看自己的订单卖家要能上架演出、管理场次和票价管理员则要管用户、管内容、管全局数据。模糊任何一个环节后期都会冒出权限绕行、数据串号的问题。这套系统的目标就是把这三点全承接住买家能浏览演出、下单购票、查看订单卖家能发布和管理演出场次处理订单核销管理员能审核内容、冻结违规账号、查看整体销售数据。三个角色对应三套菜单、三套接口、三套页面但底层共用同一套用户认证和数据库。1.2 三个角色的职责与核心操作我在实际设计功能清单的时候习惯先罗列每个角色的“用户故事”也就是他们打开系统想干什么。买家最常见的是这几件事注册登录和找回密码在首页/分类页浏览演出查看演出详情、场次时间、票价档位和剩余票数选择一个场次选择票档和数量提交订单实际项目中通常对接模拟支付或预留支付接口查看历史订单、查看未支付订单并取消、对已购票进行退票申请在个人中心修改头像、昵称、手机号等基础信息。卖家也常叫主办方或演出商的日常操作围绕“供给端”展开上架一个新演出填写演出名称、封面图、详情描述、演出地点、演出时间、分类标签给演出设置场次和票档例如某话剧在7月10日和11日各有一场每场分“A档280元”“B档180元”“C档80元”再设置每个票档的可售数量查看及核销订单一场演出开场前卖家通过订单列表或验票页面核销用户持有的票查看销售统计按演出、按场次、按日期的票房数据方便做复盘和下个档期定价。管理员的视角更偏运营和风控管理所有用户搜索、查看详情、禁用或解禁账号处理买家卖家的纠纷状态审核卖家发布的演出内容上线前检查信息是否合规、票档价格是否合理不合格的驳回去管理演出分类和首页轮播图/资讯内容查看全局数据注册用户量、总订单量、总销售额、各演出的售卖比例。1.3 典型业务流从看演出到拿到票把三个角色串起来的一条核心业务流是管理员先维护好分类和基础内容卖家创建演出并上架管理员审核通过买家在客户端看到该演出并提交订单支付成功后订单状态更新卖家在后台看到订单并于演出当天核销。这张流程图看起来直白但每一步背后都有数据表的联动。比如买家下单这一步既要查场次是否存在又要校验票档剩余数量还要在事务里同时完成“创建订单”“扣减库存”“记录明细”三个动作。任何一步失败整个订单都不能落库。我在设计页面和接口的时候都是先画完这条主链路再考虑分支逻辑。分支逻辑包括订单超时未支付自动取消恢复库存、退票后库存回流、同账号下重复限购等。把这些分支提前想好后面写代码才不会来回返工。2. 技术选型与架构设计为什么是vue nodejs2.1 前端选型Vue在表单密集场景的优势线下演出售票的后台页面信息密集型是最大特点。卖家上架演出要填一个很长的表单管理员审核也要处理大量列表和操作按钮。Vue全家桶在这一类中后台项目里确实顺手vue-router管理页面路由Pinia或Vuex管理登录态和全局数据Element Plus组件库把表格、表单、日期选择器和弹窗直接拎起来用省去了大量手写CSS和交互状态的时间。另外Vue的单文件组件机制天然鼓励你按模块拆代码——买家端、卖家端、管理端的页面各自独立通过路由和权限守卫串联代码可读性比一个页面套一堆if判断要清晰得多。2.2 后端选型Node.js Express的开发效率后端选用Node.js加Express最重要的原因是“语言统一”。前后端都用JavaScript新人不用在脑子里切换两套语言的思维方式。Express本身很轻路由和中间件模型简单直观适合快速搭建业务API。面对演出售票这种以CRUD为主的业务系统Node的异步I/O对高IO并发也有天然优势。有些团队会倾向用Spring Boot那套东西更适合大型企业级项目但语法和部署复杂度明显高一些。个人项目或者中小型商用场景Node.js方案能更快出活部署也省心一台普通服务器跑起来毫无压力。数据库方面通常用MySQL票务系统对数据一致性和事务要求高MySQL的事务支持是默认必选的理由。如果只图省事选了MongoDB文档型数据库订单库存的一致性控制就要自己在代码里多费很多功夫。2.3 整体架构和数据流向整个系统的架构分三层。前端是Vue SPA单页应用负责页面渲染、表单交互、路由控制。后端是Node.js Express的RESTful API服务负责鉴权、业务逻辑、数据库访问。数据层是MySQL存用户、演出、场次、票档、订单等核心数据。数据流向是典型的请求响应模式买家在前端点击“立即购买”前端调用后端接口后端校验身份、查数据库、扣减库存最后把订单号和状态返回给前端。前端通过axios拦截器统一携带token后端通过中间件统一校验token并解析出当前用户和角色。整条链路中前端只负责“展示和收集”后端才是“规则制定者”这样设计的好处是哪怕有人绕过页面直接调接口也无法越过权限校验。3. 数据库设计票务系统的地基3.1 核心数据表设计我整理了一张表结构清单把这个系统需要的核心表列出来每张表的字段基于实际开发中最常见的方案来设置表名关键字段说明usersid, username, password, role, phone, avatar, statusrole取buyer/seller/adminstatus控制禁用categoriesid, name, sort_order演出分类如话剧/音乐/亲子showsid, seller_id, title, cover, category_id, venue, description, status演出基本信息status为待审核/已上架/已下架sessionsid, show_id, start_time, end_time同一演出可有多场次ticket_typesid, session_id, name, price, stock, sold票档如A档/ B档可售库存ordersid, order_no, buyer_id, session_id, total_amount, status, created_at主订单order_itemsid, order_id, ticket_type_id, quantity, unit_price, seat_info订单明细paymentsid, order_id, pay_method, amount, status, paid_at支付记录或直接并入订单这七张表基本能覆盖三个角色所有的核心需求。用户表管登录角色演出和场次表达“有什么票”票档表管库存订单体系管交易。剩下像轮播图、通知公告之类的表属于业务辅助视项目规模按需加。3.2 为什么演出和场次要拆成两张表很多新手第一版设计会把“时间”直接写进演出表里比如shows表里放一个start_time。但现实场景中一场演出通常会有多个场次。比如《暗恋桃花源》巡演到某城市可能周六周日各演一场一场音乐节更夸张同一舞台三天分别售票。如果把时间直接塞进演出表一个演出就要复制多条记录每条记录还得重复存标题、封面、详情改起来灾难。拆成sessions表后一个演出对应多条场次记录每条场次记录再挂多个票档。卖家和买家看到的交互方式是“选定演出后先选场次再选票价”数据模型跟体验模型完全对上。我建议卖家上架演出时前端做成一个三级联动表单演出基本信息是一个表单场次是嵌套列表每个场次下面再嵌套票档字段数组。后端接收后在同一个事务里批量插入shows、sessions、ticket_types三张表。3.3 订单要的快照和状态机订单表里有几个设计细节容易被忽略。第一个是total_amount字段这个金额在下单那一刻必须单独存下来不能等查询时再通过票档价格去算。因为票价是会被改的演出开售后卖家临时调价老订单如果再去join票档表算金额金额就变了这就是典型的“数据没做快照”问题。order_items表里的unit_price同理存的是下单时那一刻的单价。第二个是订单状态字段。我的经验是用数字状态值枚举常见约定0待支付1已支付2已取消3已退票4已完成。比字符串更省存储也比较方便写状态流转的校验逻辑。买家能做的操作跟状态严格绑定待支付才能去支付或取消已支付才能申请退票已完成和已退票不可再操作。后端在每个状态变更接口里都要做校验否则前端绕过页面直接调接口就能把已核销的票退掉。第三个细节是关于票号。实际项目里每张售出的票最好有唯一票号类似线下票务系统的电子票编码由订单号和随机数拼接而成。这样核销时直接扫票号即可也方便后续做入场数据统计。如果票卖得多建议把order_items和票号字段独立成一张tickets表。4. 前端落地Vue项目从搭建到页面跑通4.1 环境准备和项目初始化在开始敲代码之前先把环境准备好。Node.js是Vue和Express共同的运行基础去官网下载LTS版本安装包一路默认下一步即可。安装完成后打开终端输入node -v能输出版本号就说明成了。新手最容易卡的是npm命令在PowerShell里可能直接报“npm.ps1无法加载文件因为在此系统上禁止运行脚本”这个报错别慌不是环境坏了是PowerShell执行策略默认禁止脚本运行。解决办法是以管理员身份打开PowerShell执行Set-ExecutionPolicy RemoteSigned选择Y确认重新打开终端就好了。前端脚手架新手建议用npm create vitelatest选择Vue模板比基于webpack的老式vue-cli更快、依赖更少。初始化完以后记得装项目运行需要的依赖包npm install npm install vue-router4 pinia axios element-plus项目结构我习惯按模块分目录views目录下拆buyer、seller、admin、login等子目录components目录放公共组件router目录配置路由与守卫api目录统一管理接口请求stores目录用Pinia装用户状态。这样分清楚后三个角色的页面不会互相污染协作开发时也不容易冲突。4.2 路由与动态菜单角色权限的前端防线页面结构设计时我建议把登录页作为唯一入口登录成功后后端返回用户信息和角色标识前端存储到Pinia中。然后根据角色生成不同的路由和菜单。具体实现上可以直接在meta里标记每个路由允许的角色然后给路由配置全局前置守卫。主要逻辑是两段router.beforeEach((to, from, next) { const user useUserStore(); if (!user.token to.path ! /login) { next(/login); } else if (user.token to.path /login) { next(/); } else if (to.meta.roles !to.meta.roles.includes(user.role)) { next(/404); } else { next(); } });第一段拦截未登录用户跳回登录页第二段拦截角色不匹配的用户。前端这种守卫只能防“正常用户乱点”防不了恶意请求真正的权限拦截必须做在后端前端更多是给用户提供正确导航。动态菜单可以基于路由表递归生成也可以在后端接口里返回一个菜单数组前端根据数组渲染侧边栏。后者更灵活因为不同版本的菜单可以直接由后端配置前端不用改代码。我个人更推荐后端返回菜单配置的做法做多租户或角色细分化时能省很多事。4.3 核心页面拆解与组件复用页面开发最有意思的部分是抽组件。我以几个核心页面为例说明演出列表页前后台通用买家端首页的商品卡片和卖家端的演出管理列表其实可以复用同一个演出卡片组件。区别只在于卡片上显示的操作按钮不同买家看到“查看详情”卖家看到“编辑/下架”。做法是把卡片做成本地组件通过props传入数据、通过插槽传入操作区域。售票页最容易做砸的是“选票流程”。我的方案是演出详情页放场次列表点击某一场次后右侧出现票档面板展示名称、价格和剩余数量。买家点击票档后底部弹出数量选择器最多可购数量不能超过该票档剩余库存这个前端要限制后端要同样校验。选完后点击“立即购买”跳转到确认订单页展示本单明细。订单页截屏保存方便跟后端联调时核对数据。卖家上架演出页是整个系统里表单最重的一张页面。建议用el-form配合动态增减表单项演出标题、封面、详情是一个区块场次信息是一个循环列表每条包含开始时间和结束时间每个场次内部的票档又是嵌套循环。Element Plus的el-form支持动态prop校验复杂嵌套下一定要给每个表单项设置唯一的prop路径比如sessions[0].ticketTypes[0].price否则校验规则根本匹配不上。4.4 与后端联调axios封装和接口对接axios封装是前端工程质量的分水岭。我习惯在src/api下建一个request.js统一管理import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const user JSON.parse(localStorage.getItem(user) || {}); if (user.token) { config.headers.Authorization Bearer user.token; } return config; }); request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(user); window.location.href /login; } return Promise.reject(error); } );这样所有接口请求自动携带登录凭证后端返回401时自动踢回登录页不用每个页面都手写一遍。接口函数统一放在api目录按业务拆文件比如show.js只放演出相关接口order.js只放订单相关接口。联调时如果接口报跨域开发环境建议在vite.config.js里配代理把请求转发到后端地址避免调试期反复改后端cors配置。5. 后端落地Node.js接口与核心业务逻辑5.1 项目初始化和中间件设计后端项目我建议单独建一个server目录独立于前端package.json和node_modules各管各的。初始化命令很简单npm init -y npm install express mysql2 cors jsonwebtoken bcryptjsexpress负责路由和HTTP处理mysql2负责连接MySQL数据库cors解决跨域问题jsonwebtoken签发和校验tokenbcryptjs对用户密码加密存储。如果你后续要写日志、文件上传再加上multer和morgan按需使用。项目结构按功能模块拆routes目录存放路由定义controllers目录放业务逻辑middlewares目录放鉴权等中间件config目录放数据库连接配置。很多人喜欢把所有接口全写在一个app.js里前期方便路由多了以后会非常痛苦。按模块分文件找接口快也方便多人协作。5.2 认证与权限控制后端认证建议采用JWT方案。用户注册时密码只存bcrypt加密后的密文登录时用bcrypt.compare比对明文密码与密文匹配成功后签发token。这里有个容易踩的坑token里只放用户id和角色信息不要放密码token一旦签发就是公开可读的携带数据只是难以伪造。权限控制的核心是写一个中间件const jwt require(jsonwebtoken); function auth(requiredRole) { return (req, res, next) { const token req.headers.authorization?.split( )[1]; if (!token) return res.status(401).json({ code: 401, msg: 未登录 }); try { const decoded jwt.verify(token, process.env.JWT_SECRET); if (requiredRole decoded.role ! requiredRole) { return res.status(403).json({ code: 403, msg: 无权限 }); } req.user decoded; next(); } catch (err) { return res.status(401).json({ code: 401, msg: 登录已过期 }); } }; }路由使用时这样挂router.post(/shows, auth(seller), showCtrl.createShow); router.post(/shows/:id/audit, auth(admin), showCtrl.auditShow);这样一来卖家无法调管理员的审核接口买家无法调卖家的上架接口。如果你发现某个接口忘记加auth中间件就等于裸奔接口任何人拿到API地址就能操作这是做多角色系统最需要绷紧的弦。5.3 购票主流程从校验到下单的完整代码级拆解购票这个接口是整个后端业务逻辑含金量最高的地方。核心难点在于防止超卖两个买家同时抢最后一张票如果你只是“查一下库存大于0然后插入订单”两次查询可能同时通过然后库存变成负数票卖出去了却没有库存保证。解决方案是使用MySQL事务和行级锁。简单示意如下const pool require(../config/db); async function createOrder(req, res) { const { sessionId, items } req.body; const userId req.user.id; const conn await pool.getConnection(); try { await conn.beginTransaction(); let totalAmount 0; const orderItems []; for (const item of items) { // 关键对票档行加锁防止并发修改库存 const [ticketRows] await conn.query( SELECT * FROM ticket_types WHERE id ? FOR UPDATE, [item.ticketTypeId] ); if (!ticketRows.length) { throw new Error(票档不存在); } const ticketType ticketRows[0]; if (ticketType.stock item.quantity) { throw new Error(库存不足); } await conn.query( UPDATE ticket_types SET stock stock - ?, sold sold ? WHERE id ?, [item.quantity, item.quantity, item.ticketTypeId] ); totalAmount ticketType.price * item.quantity; orderItems.push({ ticketTypeId: item.ticketTypeId, quantity: item.quantity, unitPrice: ticketType.price }); } const orderNo T Date.now() String(Math.random()).slice(2, 8); const [orderResult] await conn.query( INSERT INTO orders (order_no, buyer_id, session_id, total_amount, status) VALUES (?, ?, ?, ?, 0), [orderNo, userId, sessionId, totalAmount] ); // 批量插入订单明细、扣减库存 // ... 此处省略明细插入循环 await conn.commit(); res.json({ code: 0, data: { orderNo, totalAmount } }); } catch (err) { await conn.rollback(); res.json({ code: 1, msg: err.message }); } finally { conn.release(); } }SELECT ... FOR UPDATE是MySQL提供的行锁在事务中先锁住票档记录其他事务的相同行锁查询就会等待直到当前事务提交或回滚。这样能有效防止超卖。我在项目中实测过压测20个并发请求抢同一票档的最后2张票只有2个请求成功其余全部返回库存不足。5.4 基于角色的业务接口规划角色决定了接口划分接口规划可以做成一张对照表后端开发时按表逐条实现角色接口模块示例接口公共接口认证模块POST /api/auth/register, POST /api/auth/login买家接口演出浏览与订单GET /api/shows, GET /api/shows/:id, GET /api/orders/my, POST /api/orders卖家接口演出与场次管理POST /api/shows, PUT /api/shows/:id, GET /api/shows/my, POST /api/shows/:id/sessions, POST /api/orders/:id/verify管理员接口用户与系统管理GET /api/users, PUT /api/users/:id/status, GET /api/shows/pending, PUT /api/shows/:id/audit, GET /api/stats/overview业务上可以按这个思路走所有人能看已上架演出的公共信息买家只能在登录后操作自己的订单卖家只能操作自己创建的演出管理员能看到全局。后端在每个查询接口里都用req.user.id去拼where条件这样即使用户手写请求也只能拿到自己范围内的数据。6. 常见问题与踩坑实录新手最容易翻车的几个点6.1 npm.ps1无法加载文件Windows下的PowerShell执行策略这个问题在Windows环境出现的概率极高。错误信息是“npm.ps1因为在此系统上禁止运行脚本”。原因在于PowerShell默认执行策略是Restricted不允许任何脚本文件运行。解决办法以管理员权限打开PowerShell执行Set-ExecutionPolicy RemoteSigned然后输Y回车。这条命令允许本地脚本运行远程下载的脚本如果没签名依然会被拦。如果公司电脑管理员权限受限临时办法是改在CMD命令提示符里跑npm命令CMD没有这个限制。6.2 跨域问题与开发环境代理前后端分离开发时前端跑在5173端口后端跑在3000端口浏览器直接发请求会被CORS拦下。后端装cors中间件能解决一部分问题但开发环境我更推荐用Vite代理来转发在vite.config.js里配置server.proxy把/api开头的请求转发到http://localhost:3000。这样做的好处是前端代码里所有请求统一用相对路径/api/xxx等部署时把dist目录交给Nginx做反向代理同样带/api前缀的请求转到Node服务前端代码完全不用改。实现时注意代理路径的路径重写规则生产环境Nginx配置类似location /api/ { proxy_pass http://127.0.0.1:3000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样浏览器只认识你的域名和端口后端接口的端口和路径全部隐藏安全性也更好。6.3 路由守卫失效的常见原因路由守卫写了但页面刷新后守卫不生效或者跳转到404我见过最多的两种原因。第一种是Pinia或Vuex的state在页面刷新后重置了token信息丢失一刷新就被踢回登录页。解决办法是把token和用户信息持久化到localStorage或者用Pinia的persist插件。第二种是动态路由里meta.roles没写或者写错数组关系导致守卫判断出错。建议把角色标识统一用小写字符串buyer/seller/admin不要在有的地方写大写有的地方写首字母大写字符串比较大小写敏感这个坑很隐蔽但非常好踩。6.4 前端展示图表Vue中接入ECharts卖家后台和管理员后台都离不开销售统计图表。最常用的方案是在Vue里封装一个ECharts组件按需引入图表类型。以柱状统计图为例用ref监听DOM节点在onMounted里初始化echarts实例传入配置对象如统计某场演出每天的售票数import * as echarts from echarts; onMounted(() { const chart echarts.init(chartRef.value); chart.setOption({ xAxis: { type: category, data: dates }, yAxis: { type: value }, series: [{ type: bar, data: salesData }] }); window.addEventListener(resize, () chart.resize()); });要注意的是组件卸载前调用echarts.dispose释放实例避免内存泄漏异步获取数据时要等接口返回后再setOption不要同步传空数组。多图表共存时给每个图表初始化不同的ref变量不要把两个图表挂到同一个DOM节点上。6.5 部署与端口隐藏从本地到服务器项目开发完要上线演示很多人卡在部署这一步。前端打包命令npm run build生成的dist目录是纯静态文件可以直接交给Nginx托管后端代码整体上传服务器执行npm install --production以及node app.js启动。为了后端进程不因窗口关闭就断掉建议用pm2管理进程npm install -g pm2 pm2 start app.js --name show-ticket-api pm2 save pm2 startup这样开机自动重启进程挂了也会自动拉起。前后端在同一台服务器上Nginx托管前端静态文件并反向代理/api请求。买家访问的永远只有一个域名和80/443端口后端服务的实际端口在外部完全不可见。如果项目是给别人演示的毕设这种部署方式看起来非常完整。7. 实操总结与个人体会做这个项目的过程中我最大的体会是多角色系统真正难的不是某一个页面、某一个接口而是业务状态和数据一致性在各处都要对齐。前端限制了买家不能下架演出后端还要再拦一次卖家改了票档价格老订单的金额不能跟着变两个买家抢同一张票数据库层面必须有锁。这些细节如果只靠“写完功能看起来能跑”来验收迟早会在演示现场出丑。另外给准备拿这套系统当面试项目或毕业设计的朋友一个建议不要止步于把CRUD跑通。可以在现有的基础上再加两个有亮点的模块比如基于二维码的验票核销、基于ECharts的票房多维统计报表、基于定时任务的超时未支付自动关单。这些功能做起来难度适中但能明显拉升项目上限。面试时被问“你项目里最难的点是什么”你有下单事务防超卖、动态菜单权限、Nginx部署这些真实经历过的问题可讲绝对不是背概念能比的效果。最后说个经验之谈开发多角色系统前一定要先把角色权限矩阵和状态流转图画出来。我第一版项目没画代码写着写着就出现了“客户账号能看见卖家菜单”这种低级事故。重新梳理后把权限矩阵贴在桌面后面所有的页面跳转和接口校验都对着它写乱子立刻变少。这比任何框架和技巧都好使。
返回列表