简介:这是一套基于JavaScript与Vue开发的银行预约管理系统前后台源码,属于高分毕业设计项目,评审得分95分,面向计算机、自动化等相关专业学生,可直接用于毕业设计、课程大作业或期末实训。系统包含人员管理、银行管理、业务管理和预约管理四大核心模块,前端采用Vue管理后台模板,后端逻辑清晰,适合学习前后端分离开发、预约流程设计与权限控制。压缩包共374个文件,以vue组件、js脚本、json配置为主,另含png图片、scss样式及markdown文档等,整体体积仅3.26MB,目录结构规整,便于快速定位与小范围修改。截至目前已有136人学习使用。资源已调试至可运行状态,解压后按启动命令即可体验;适合在此基础上扩展预约类型、增加数据统计或对接后端接口,进一步提升项目完整度与创新性。
1. 银行预约管理系统:用 Vue 把「排队」变成「预约」
银行网点排队是刚需场景,但传统现场取号在高峰时段经常把大堂挤满,用户等一小时办三分钟是常态。预约制的思路是把「人到现场再排队」改成「线上预约、按时到店、窗口直接办理」,省掉无效等待。这套系统在前端由 JavaScript + Vue 承担:用户前台负责预约取号、时段选择和订单查询,管理后台负责窗口调度、叫号与统计,前后台共用一套组件与接口封装。对做毕业设计的人来说,它的价值在于业务链路完整,从用户预约到柜员办理再到报表输出,每个环节都有可展示的代码;对小型网点或政务大厅的信息化改造,这套前后台结构也能直接裁剪复用。无论是自己从零实现,还是拿到源码后二次梳理,下面的方案都按「能复现、能答辩、能扩展」三条线展开。
2. 预约管理系统的前后台拆分与 Vue 路由权限设计
2.1 用户前台和管理后台:共用工程还是两套独立应用
做银行预约管理系统,第一步要定工程结构。常见做法是「一个 Vue 工程、两个入口」:把用户预约端(前台)和网点管理端(后台)放在同一个仓库里,通过路由模块和目录区分。这样做的直接好处是能共用 axios 封装、基础表单组件和样式变量,答辩时也能讲清楚「哪些代码被复用了、复用的依据是什么」。如果团队要求前后台独立部署,也可以拆成两个 Vue 项目,代价是公共逻辑要抽成 npm 包或复制两份,日常维护成本明显更高。
我一般会用这样的目录组织前后台代码:
src/ ├─ api/ # 接口层 │ ├─ portal.js # 前台预约接口 │ └─ admin.js # 后台管理接口 ├─ router/ │ ├─ index.js # 路由总表与全局守卫 │ ├─ portal.routes.js # 前台路由 │ └─ admin.routes.js # 后台路由 ├─ views/ │ ├─ portal/ # 预约页、订单详情 │ └─ admin/ # 窗口管理、叫号台、报表 └─ components/ ├─ common/ # 跨前后台复用 └─ admin/ # 后台专用组件这段结构里最关键的一点是:用户看的预约页和柜员看的窗口页从路由层就分开,而不是在同一个页面里用 v-if 切来切去,否则菜单权限、组件懒加载和路由参数会互相污染。前后台真正共用的只有按钮、弹窗、表单这类基础组件,预约表单和叫号队列这种业务组件不要跨域复用,因为它们的接口协议和状态流转完全不同。
2.2 用 Vue Router 守卫把角色鉴权做在进页面前
银行预约系统对权限的敏感点不在页面隐藏,而在路由和接口双把关。前台只需要校验用户是否登录,后台则要区分管理员、柜员、网点主管三种角色。把判断收敛在 router.beforeEach 里,比在每个页面 mounted 里各写一遍登录判断更省事,也能覆盖「手动输入 URL 直达后台页面」的越权路径。
// router/index.js const whiteList = ['/portal/home', '/login'] // 免登录白名单 router.beforeEach((to, from, next) => { const token = localStorage.getItem('bank_token') const role = localStorage.getItem('bank_role') // user / admin / teller if (!token) { if (whiteList.includes(to.path)) return next() return next({ path: '/login', query: { redirect: to.fullPath } }) } // 页面 meta.roles 配置了角色要求时才校验 if (to.meta.roles && !to.meta.roles.includes(role)) { return next({ path: '/403' }) } // 预约完成后带回来源网点页 if (to.query.redirect) { return next({ path: to.query.redirect }) } next() })这个守卫分三段逻辑。第一段处理未登录:白名单里的页面直接放行,其余跳登录页,并用 redirect 参数记住用户原本想去的完整路径,登录成功后再跳回来。这里有个细节,redirect 要放在 query 上而不是 path 上,因为登录页本身可能也带 query;用 vue 路由参数传递后,在登录页取出再放回跳转目标即可。第二段做角色校验:前台路由的 meta.roles 配成['user'],后台路由配成['admin','teller'],就算手输 /admin/window 也会被拦到 /403。第三段是预约场景的特例,预约成功后从订单详情返回网点首页,避免用户点浏览器后退丢表单状态。
注意 role 不能只信 localStorage,每次进入后台前要调一次 /user/info 刷新真实角色,防止改本地缓存绕过校验。真正的兜底在后端接口,token 解析出的角色才作数,前端校验只是体验层。
2.3 预约系统的核心数据模型与预约订单表设计
预约管理系统的数据模型是典型的「业务类型 → 窗口 → 号源 → 订单」四层关系,表结构直接决定代码复杂度。下面是一套经过验证的最小表结构:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| business_type | id, name, avg_duration | 业务类型,如开户、挂失、对公 |
| window_info | id, window_no, biz_type_id, status | 窗口,动态绑定一种业务类型 |
| appointment_order | id, order_no, user_id, biz_type_id, window_id, appoint_date, time_slot, queue_no, status | 预约主表 |
| queue_ticket | id, order_no, window_id, call_time, finish_time, status | 叫号轨迹,一个预约对应一条 |
这里最容易疏忽的是 appointment_order 要存 window_id 而不是只存 biz_type_id。用户预约的是「某个窗口的某个时段」而非「某个业务」,窗口和业务的绑定关系是后台随时可能改的,若只存业务类型,改绑后已生成的预约号就会错乱。queue_ticket 单独建表,是为了叫号时不动预约主表、每次叫号办结都留一条轨迹,统计平均办理时长直接查这张表,不用在预约订单上堆状态字段。
3. 预约取号流程:用 JavaScript 切号源、做校验与排队号分配
3.1 把一天切成时间段:号源生成策略
预约系统第一个难点是「号什么时候算有」。银行窗口不是电影院座位,不能只控制全天总量,要精确到时间段。常见做法是按业务类型的平均办理时长切分营业时间:一个窗口 9:00–12:00、14:00–17:00 营业,业务平均办理 15 分钟,就把上午切成 12 个时段,每个时段放 2 个号,避免某一小时挤进一堆人、其他时段空转。
号源不必提前落库,前台组件按日期动态生成即可,切段逻辑如下:
// utils/slot.js export function buildSlots(bizType, windows, date) { const slots = [] for (const win of windows) { for (const segment of bizType.workSegments) { // [{ start: '09:00', end: '12:00' }] let cur = segment.start while (cur < segment.end) { const end = addMinutes(cur, bizType.avgDuration) // 按平均办理时长步进 if (end > segment.end) break // 超营业段直接截断 slots.push({ windowId: win.id, date, start: cur, capacity: bizType.slotCapacity, // 每个时段放几个号 booked: loadBookedCount(win.id, date, cur) }) cur = end } } } return slots }这段代码的重点在 while 循环的步进与截断:cur 每次累加 avgDuration,若 end 超过营业段末尾就 break,这样最后一个时段不会出现「开始正常、结束超营业时间」的脏数据。booked 字段用于前端置灰,把 capacity 减 booked 小于等于 0 的时段设置 disabled。前端置灰只是体验层,真正的并发控制在接口:后端在同一事务里用条件更新扣减号源,防止两个用户同时锁住最后一个号。
3.2 预约表单的表单校验、防重复提交与时间选择
预约表单通常只有业务类型、时间段、手机号三个必填项,加一个备注选填。但越简单越容易出重复提交——用户点击提交后等接口响应时又点了一次,就会产生两条订单。Vue 里防两层:按钮 loading + 提交函数内标志位。
// views/portal/AppointmentForm.vue const submitting = ref(false) async function handleSubmit() { if (submitting.value) return submitting.value = true try { const { orderNo } = await createOrder({ bizTypeId: form.bizTypeId, windowId: form.windowId, appointDate: form.appointDate, timeSlot: form.timeSlot, mobile: form.mobile.trim() }) ElMessage.success(`预约成功,排队号 ${orderNo}`) router.push({ path: '/portal/detail', query: { orderNo } }) } finally { submitting.value = false } }submitting 是响应式标志位,进入提交立即置 true,后续点击直接 return,finally 保证失败后能复位。它比单纯给按钮加 disabled 可靠,因为 disabled 在组件重新渲染的间隙会短暂失效,而标志位不依赖 DOM 状态。接口层再配合一个请求拦截器,对 5 秒内相同参数的请求做去重,双保险才算闭环。
校验规则里,手机号要做 1 开头 11 位的正则校验,同时去掉首尾空格再判断,不能只看长度;时间选择用 el-date-picker 配合 disabledDate 把过去的日期禁掉,再结合时段列表做二次限制。表单整体用 el-form 的 rules 声明式配置,提交时 validate 通过才发请求。
3.3 排队号生成规则与预约状态流转
预约提交成功后要生成排队号。排队号必须满足「同一天、同窗口唯一且可读」,常见规则是日期 + 窗口号 2 位 + 当日序号 3 位,例如 20250611-03-007。序号必须由后端基于 appointment_order 表里同窗口、同日期最大序号加一生成,并在表上加唯一索引兜底,不能靠前端时间戳或随机数拼,否则并发下会产生重复号。号源扣减与订单创建要在同一事务里完成,任一步失败都要回滚并释放号源。
状态流转是预约系统的第二个易错点。预约创建后是「已预约」,柜员叫号变「已到号」,办理中、已完成、已取消是后续态。需要特别区分「用户主动取消」和「窗口过号自动取消」:主动取消要立刻释放号源,过号自动取消则要结合网点策略决定是否允许重排到队尾。完整状态机如下:
| 当前状态 | 触发动作 | 下一状态 |
|---|---|---|
| 已预约 | 柜员点击叫号 | 已到号 |
| 已到号 | 用户 3 分钟内未签到 | 已过号 |
| 已到号 | 柜员点击开始办理 | 办理中 |
| 办理中 | 柜员点击完成 | 已完成 |
| 已预约 / 已到号 | 用户取消 | 已取消 |
已过号不一定直接作废,很多网点允许用户重新排队,所以 queue_ticket 里额外留一个 reorder 字段记录重排次数,前端的队列列表按「未叫号 + 已过号」排序展示,两个分组之间用 Vue 的 computed 派生,不要手动改数组。
提示:取消预约的接口要做幂等处理,用户重复点击取消或后台重试时,第二次请求应直接返回成功而不是报错,否则前端容易出现「明明取消了还提示取消失败」。
4. 管理后台的窗口调度、叫号逻辑与 Vue 统计报表
4.1 窗口与业务类型的动态绑定
管理后台的核心操作是窗口管理。网点可能有 8 个窗口,但高峰只开 4 个、低峰只开 2 个,所以窗口要支持动态启停和改绑业务。管理员的操作流程是:选窗口、选业务类型、保存。保存时后端除了更新 window_info,还要检查该窗口是否存在未完成预约,若有则前端弹确认框,提示「还有 3 个预约未办理,停用后将转入待分配池」,这是银行网点「窗口可以关、业务不能断」的运营要求。窗口与业务绑定关系对应三种状态:
| 窗口状态 | 含义 | 是否可被预约 |
|---|---|---|
| active | 正常营业 | 是 |
| closed | 临时关闭 | 否 |
| relocating | 停用且清理未完成预约中 | 否 |
窗口状态变更的代码本身不复杂,但检查顺序不能乱:
async function toggleWindow(win) { if (win.status === 'active') { // 先查未完成预约,再决定是否允许停用 const pending = await api.getPendingOrderCount(win.id) if (pending > 0) { const confirm = await ElMessageBox.confirm( `该窗口还有 ${pending} 个预约未办理,停用后将转入待分配,确认停用?` ) if (!confirm) return } } const res = await api.setWindowStatus(win.id, win.status === 'active' ? 'closed' : 'active') win.status = res.status }这里调用的关键参数是 windowId,所有接口都要以窗口维度查询,而不是以业务类型维度。停用后转入待分配的具体做法:把 appointment_order 中该窗口未完成记录的 window_id 置空,由管理员按业务类型手动重绑,或由系统按「同业务类型、最早开始时间」自动分配。注意这个更新必须是带条件 UPDATE,只更新仍指向旧窗口的记录,避免把别的窗口的预约一起改掉。
4.2 叫号逻辑与队列的实时轮询
柜员端的叫号页面是后台里交互最频繁的模块。流程是:柜员登录后看到本窗口待办理列表,点「叫号」广播给用户端大屏,用户到窗口后点「开始办理」,办完点「完成」。这里要区分叫号和签到:叫号是柜员发起的广播动作,签到是用户到窗口的确认动作,两者之间是一个时间窗口,过了窗口就算过号。
实时性是这个模块的难点。银行网点网络环境复杂,叫号模块不建议依赖 WebSocket,更稳的是轮询加短连接:柜员端每 2 秒拉一次队列,用户端大屏每 3 秒拉一次叫号。Vue 组件在 onBeforeUnmount 里清理定时器,避免路由切换后请求堆积:
let poll = null onMounted(() => { poll = setInterval(async () => { const { data } = await api.getWindowQueue(windowId) queue.value = data // 重新拉取队列并覆盖 }, 2000) }) onBeforeUnmount(() => clearInterval(poll)) // 页面销毁必须停掉轮询轮询间隔要按端区分:柜员端 2 秒、网点大屏 3 秒、用户手机 5 秒。间隔太短接口压力大,太长用户觉得卡。2 秒轮询、100 个并发连接时,QPS 大约 50,普通后端毫无压力。叫号动作本身是一次条件更新,把 queue_ticket 从「已预约」改成「已叫号」并记录 call_time,SQL 里必须带 status = '已预约' 条件,防止两个柜员同时叫到同一个号。
4.3 用 ECharts 输出网点忙闲度报表
管理后台最有说服力的加分项是忙闲度报表。数据从 queue_ticket 聚合,按小时统计叫号量和平均办理时长,再用 ECharts 渲染。这个模块在答辩里能同时展示数据建模与可视化两个能力,而且数据链路完全来自前面的订单与叫号表,逻辑自洽。Vue 里安装 echarts 依赖时用npm i echarts -S,装进 dependencies 而不是 devDependencies,因为图表渲染属于运行时逻辑。
聚合查询通常是这个形态:
SELECT HOUR(call_time) AS h, COUNT(*) AS cnt, AVG(TIMESTAMPDIFF(MINUTE, call_time, finish_time)) AS avg_min FROM queue_ticket WHERE DATE(call_time) = ? AND status IN ('done', 'cancel') GROUP BY HOUR(call_time)过滤条件用 status IN ('done','cancel'),是为了把「已过号」这类被叫号但未办理的记录也纳入整体统计而不是丢弃,否则平均办理时长会偏高。前端配置 ECharts 时,要点是双 y 轴:
const chart = echarts.init(document.getElementById('busyChart')) const option = { tooltip: { trigger: 'axis' }, legend: { data: ['叫号量', '平均办理时长'] }, xAxis: { type: 'category', data: hours }, yAxis: [ { type: 'value', name: '叫号量' }, { type: 'value', name: '平均时长(分钟)' } ], series: [ { name: '叫号量', type: 'bar', data: counts }, { name: '平均办理时长', type: 'line', yAxisIndex: 1, data: avgMinutes } ] } chart.setOption(option)最常画错的地方是第二个 series 必须写 yAxisIndex: 1,否则折线和柱状图共用同一个量纲,平均时长会被叫号量带成一条横线。图表容器的高度要显式设置,组件里的样式用 scoped 隔离,避免和其他页面的 canvas 互相覆盖。报表页加一个日期选择器,默认显示最近 7 天,按天切换结果并做前端缓存,避免重复请求。
5. 银行预约管理系统的高分演示与验证技巧
拿到这类系统源码后,第一步不是逐行读代码,而是先按「一个演示脚本 + 一个异常验证清单」把它跑通。演示要在五分钟内讲完:开两个无痕窗口,一个模拟用户、一个模拟柜员,无痕窗口之间 localStorage 完全隔离,不用反复退出登录。演示顺序固定为「用户预约 → 后台窗口叫号 → 用户端看到叫号 → 开始办理 → 完成 → 报表数字变化」,每步停 10 秒左右,让评审看清状态变化。
答辩追问通常集中在三处:
| 追问点 | 回答要点 |
|---|---|
| 并发时会不会重复发号 | 后端同窗口同日最大序号加一,唯一索引兜底 |
| token 过期怎么处理 | axios 拦截器捕获 401 后用 refreshToken 刷新,失败再跳登录 |
| 停用窗口时未办理预约怎么办 | window_id 置空进入待分配池,管理员按业务类型重绑 |
验证部分建议打开浏览器开发者工具的 Network 面板,把网络切到 Slow 3G,重点看三件事:预约提交时按钮 loading 态是否全程生效、接口失败后表单数据有没有丢失、取消预约后重新预约同一时段号源是否立刻释放并恢复可见。这三条顺序执行,基本能把状态机、防重复提交、号源释放三个最容易出问题的环节全部覆盖。
最后补一个数据层的验证:查 queue_ticket 表,确认每一条叫号记录都有对应的 call_time 和 finish_time,且 finish_time 不小于 call_time,时间差落在该业务类型的 avgDuration 合理范围内。遇到时间倒挂的记录,直接回查对应的 appointment_order 状态,通常能定位到叫号接口没做状态校验的代码分支。
本文还有配套的精品资源,点击获取