news 2026/9/17 15:16:31

基于SpringBoot+Vue的公交运营管理系统:三角色权限与Token鉴权

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SpringBoot+Vue的公交运营管理系统:三角色权限与Token鉴权

简介:这是一份面向高校计算机专业毕业设计场景的完整论文文档,主题为基于SpringBoot与Vue的城市公交运营管理系统,适合正在准备毕设、课程设计或需要参考前后端分离项目写法的学生与开发者。压缩包内共1个docx文件,约4.68MB,即论文正文一份,涵盖摘要、中英文Abstract、绪论、系统设计与实现等标准章节,可按毕业论文格式直接参考或二次修改。内容围绕公交员、调度员、管理员三类角色的权限划分展开,涉及注册登录、公交调度、紧急上报与调度、车辆状态查询、线路分类、车辆信息维护等模块,并说明SpringBoot后端与Vue前端的结合方式,以及数据库设计、业务逻辑处理、身份认证、权限控制与数据加密传输等安全考量。目前已有67人学习下载。对于需要确定选题方向、梳理系统模块结构、撰写需求分析与章节框架的读者,可借此了解一个中小型管理系统的完整论文组织方式与功能描述粒度,也可作为文档排版与图表安排的对照样本。

1. 从早班车排班说起:公交运营管理系统要解决什么

凌晨四点五十,调度员要在首班车发车前把当天的车辆、司机、线路对齐,传统做法是拿一张纸质排班表挨个打电话确认,碰上车辆故障就得把整套安排推倒重来。城市公交运营管理系统处理的就是「人—车—线路—时间」这四者对齐的问题:车辆档案进系统,调度单落在库里,故障有上报入口,紧急情况有独立流转通道,公交员、调度员、管理员各自看到自己该看的那一块。它不是一个炫技的项目,而是一套典型的中小型信息管理系统:角色清晰、表结构规整、CRUD 密集、流程可控。SpringBoot 负责把后端接口和鉴权串起来,Vue 负责把三个角色的工作台拆成互不干扰的页面。适合拿它练手的人大致分两类:正在做 Java 毕业设计、需要一个能讲清楚业务又跑得起来的学生;以及刚接触前后端分离、想拿一个完整角色权限模型练手的初级开发。

2. 三角色权限模型与 Token 鉴权的后端实现

权限这一层如果用不好,后面所有业务模块都会写歪。这套系统最值得先啃下来的部分,就是它怎么用最少的表结构撑起三个角色的差异化操作,以及登录之后状态是怎么维持的。

2.1 三个角色的权限边界怎么划

管理员、调度员、公交员的功能重叠度不低,公交调度和车辆状况三个角色都会碰,但动词不同:管理员是增删改查,调度员是新增和查看,公交员是查看和上报。把权限差异拆成「模块 × 操作」的矩阵,比给每个角色单独写一套接口要省事得多。

功能模块管理员调度员公交员
个人中心查看 / 改密查看 / 改密查看 / 改密
公交员管理增删改查 / 审核不可见不可见
调度员管理增删改查 / 审核不可见不可见
线路分类管理增删改查不可见不可见
公交车辆管理增删改查新增 / 查看不可见
公交调度管理增删改查新增 / 查看查看
紧急上报管理审核 / 查看查看提交 / 查看
紧急调度管理增删改查新增 / 查看查看
车辆状况管理增删改查查看上报 / 查看

常见做法是直接在后端把角色当成一个字符串枚举存进用户表,接口层不做细粒度切分,而是在拦截器里按「路径前缀 + 角色」做粗粒度放行,业务层再补一次归属校验。这样表结构简单,代价是权限规则散落,后期加角色要改多处,这一点在选型时要想清楚。

2.2 登录与 Token 签发:先让状态落库

登录不是签个 JWT 就完事,这套系统把 token 显式存进了库表,好处是服务端可主动失效,一个账号在手机和电脑上各登一次也不会互相踢掉。表里需要的关键字段是useridusernametablenameroletokenaddtimeexpiratedtime,其中tablename用来标记这个账号来自哪张角色表,role用来标记角色,两者配合就能在鉴权时还原出完整身份。

@PostMapping("/login") public R login(@RequestBody LoginForm form, HttpServletRequest request) { // 1. 按用户名 + 角色定位账号,角色决定去查哪张表 TableName table = TableName.valueOf(form.getRole()); UserEntity user = userService.selectByUsername(table, form.getUsername()); if (user == null || !user.getPassword().equals(MD5Util.md5(form.getPassword()))) { return R.error("账号或密码不正确"); } // 2. 同角色并发登录时,先清掉旧 token,避免一张表出现多条有效记录 tokenService.deleteByUserid(table, user.getId()); // 3. 生成 token 并落库,有效期统一设为 2 小时 String token = UUID.randomUUID().toString().replace("-", ""); TokenEntity entity = new TokenEntity(); entity.setUserid(user.getId()); entity.setUsername(form.getUsername()); entity.setTablename(table.getValue()); entity.setRole(form.getRole()); entity.setToken(token); entity.setExpiratedtime(DateUtil.addHours(new Date(), 2)); tokenService.insert(entity); return R.ok().put("token", token); }

逻辑上分三步:定位账号、清旧 token、签发新 token。参数里role是整个链路的分叉点,前端登录页必须让用户显式选择角色,不能从账号格式去猜。expiratedtime设成固定两小时而不是随机,是为了让前端能在本地先判断过期、主动跳登录页,减少一次 401 往返。密码存 MD5 只是够用级别,真上线要换成 BCrypt 并加盐,这一点在做毕设答辩时也常被问到。

2.3 拦截器按角色放行

鉴权放在拦截器里做,业务代码就不用每个方法都写一遍校验。

public class AuthorizationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行登录、注册、静态资源 if (isPublicUri(request.getRequestURI())) return true; String token = request.getHeader("Token"); TokenEntity entity = tokenService.selectByToken(token); // 1. token 不存在或已过期,直接 401 if (entity == null || entity.getExpiratedtime().before(new Date())) { response.setStatus(401); return false; } // 2. 续期:每次访问把有效期往后推 2 小时,避免操作中被踢 tokenService.refresh(entity.getId()); // 3. 把角色写进 ThreadLocal,业务层用角色做二次收口 SessionContext.set(entity.getRole(), entity.getUserid(), entity.getTablename()); return true; } }

三个关键点:一是isPublicUri的白名单要精确到接口,别用/api/**这种一把梭;二是续期放在拦截器而不是过滤器,方便和 token 表操作复用同一个 service;三是ThreadLocal里的角色信息在afterCompletion里必须清掉,Tomcat 的线程池会复用线程,不清就是跨请求的数据污染,这个坑在并发测试时才会暴露。

3. 公交调度、紧急上报与车辆状况:业务链路怎么串

三个业务模块看着独立,实际是一条链:调度排班 → 车辆出状况 → 上报 → 紧急调度补位。把链路讲明白,表字段怎么设就自然清楚了。

3.1 公交调度:车辆、线路、司机三方绑定

公交调度表要回答的问题是「某辆车的某个班次由谁开、跑哪条线」。所以核心字段是gongjiaochehao(公交车号)、chepaihao(车牌号码)、sijigonghao(司机工号)、sijixingming(司机姓名)、banci(班次)和diaodushijian(调度时间)。

调度员新增一条调度时,前端要两级联动:先选线路分类,再拉出该线路下的车辆列表。这里不要在下拉框里一次性把全量车辆查出来,车多的时候接口会明显变慢。

// 线路变化后按线路分类拉车辆,前端做防抖,避免连续点击打爆接口 async handleLineChange(lineId) { this.vehicleOptions = []; if (!lineId) return; const res = await getVehicleListByLine({ lineId, page: 1, limit: 200 }); // 只回传必要字段,减少响应体体积 this.vehicleOptions = res.data.list.map(v => ({ value: v.gongjiaochehao, label: `${v.chepaihao} / 座位 ${v.zuoweishuliang}`, chepaihao: v.chepaihao, piaojia: v.quanchengpiaojia })); }

limit: 200是权衡后的取值,公交车辆一般在几百辆量级,一次拉完比拼分页体验好;如果线路下车辆超过这个数,就得改成分页搜索。chepaihaopiaojia跟随车辆带出来直接回填到表单,是为了减少调度员的手工输入,同时防止车牌号被手改导致和车辆档案对不上。

3.2 紧急上报到紧急调度:状态怎么流转

紧急上报和紧急调度是两张表,不是一张表加个状态字段。原因很实际:上报是公交员的动作,调度是调度员或管理员的动作,两者的操作人、操作时间、可编辑字段完全不同,硬塞进一张表会让权限判断变成一团乱麻。

阶段操作人数据落表关键字段变化
发起上告公交员紧急上报表sfsh置为「待审核」,shhf为空
审核处理管理员紧急上报表sfsh改为「已审核」,写入shhf
生成调度调度员紧急调度表带入gongjiaochehaosijigonghao,写入diaoduanpai
执行反馈调度员紧急调度表diaodushijian落定,车辆回归正常排班

流转过程中最容易出问题的是「同一辆故障车被重复调度」。常见做法是在生成紧急调度前,先按gongjiaochehao查一次未完成的紧急调度记录,命中就拒绝写入并返回提示,这个校验必须放在服务端,前端置灰按钮挡不住并发。

3.3 车辆状况:故障上报与闭环

车辆状况表里sfgz(是否故障)是字符串而不是布尔值,字段是varchar(200),取值为「是」「否」。这么做牺牲了索引效率,换来的是前端渲染和审核回复能共用一套模板,属于典型的毕设取舍。

@Transactional public void reportStatus(VehicleStatusForm form) { // 1. 写入车辆状况记录 vehicleStatusService.insert(form.toEntity()); // 2. 标记故障时,同步把车辆状态改为不可调度,防止继续排班 if ("是".equals(form.getSfgz())) { vehicleService.updateStatus(form.getGongjiaochehao(), "故障"); // 3. 同一辆车的未完成调度单全部置为异常,等待人工重排 scheduleService.markAbnormal(form.getGongjiaochehao()); } }

@Transactional是因为这三步要么都成、要么都不成。如果第二步失败而第一步提交了,就会出现「车辆状态显示正常但已经有故障上报」的脏数据。第三步的markAbnormal用批量 update 而不是循环单条,车辆多的时候差别很明显。

4. 表结构落地与查询实现细节

表设计这部分最容易被忽略,但它决定了后面写业务代码顺不顺。

4.1 字段类型的几个实际选择

从数据表看,这套系统的字段设计有几个稳定习惯:主键统一bigint自增,时间字段统一timestamp DEFAULT CURRENT_TIMESTAMP,业务字段一律用varchar(200),长文本用longtext且长度写 4294967295。

字段类型说明注意点
idbigint主键自增,不做业务含义
addtimetimestamp创建时间默认当前时间,代码里不再赋值
gongjiaochehaovarchar(200)公交车号业务主键,需加唯一索引
chepaihaovarchar(200)车牌号码关联车辆档案,不做外键
shangbaoxiangqinglongtext上报详情长文本,别用在 where 条件里
sfshvarchar(200)是否审核默认「待审核」,值域靠代码约束
tokenvarchar(200)登录令牌需加索引,拦截器每次请求都查
expiratedtimetimestamp过期时间拦截器判定依据

两个必踩的坑:token字段默认值是CURRENT_TIMESTAMP,手工造数据时必须显式写入,否则刚创建的 token 立刻就是过期状态;expiratedtime同理会默认取当前时间,代码里如果不覆盖,登录后第一次请求就被踢,日志里表现为「刚登录就 401」,排查时容易误判成前端没带 header。

4.2 逻辑关联而非物理外键

公交调度、紧急调度、车辆状况三张表都冗余了chepaihaosijixingmingsijigonghao,而不是只存 ID 做关联。原因是这些是展示字段,列表页要一次性把车牌、司机姓名都渲染出来,用 join 查三张表在大数据量下明显更慢。代价是司机改名后历史记录不会跟着变——对调度留痕场景来说,这反而更贴近实际需求,因为要记录的是「当时是谁在开车」。

4.3 条件分页查询的写法

列表页几乎都有时间范围 + 关键字 + 分页三个条件,用 MyBatis-Plus 的条件构造器写比手写动态 SQL 更省事。

public PageUtils queryPage(Map<String, Object> params) { // 1. 取出非空查询条件,空字符串要过滤掉,否则会拼出 like '%%' 全表扫 String chepaihao = (String) params.get("chepaihao"); String startTime = (String) params.get("startTime"); String endTime = (String) params.get("endTime"); QueryWrapper<EmergencyReportEntity> wrapper = new QueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(chepaihao), "chepaihao", chepaihao); // 2. 时间区间用 ge/le,注意数据库里是 datetime,前端传的是 yyyy-MM-dd HH:mm:ss wrapper.ge(StringUtils.isNotBlank(startTime), "shangbaoshijian", startTime); wrapper.le(StringUtils.isNotBlank(endTime), "shangbaoshijian", endTime); wrapper.orderByDesc("addtime"); // 3. 分页参数来自前端,默认第 1 页、每页 10 条 IPage<EmergencyReportEntity> page = this.page( new Query<EmergencyReportEntity>().getPage(params), wrapper); return new PageUtils(page); }

like的第一个参数是条件开关,为空时不会拼进 SQL,这是避免全表扫描的第一道闸。时间区间用ge/le而不是between,是为了允许前端只传开始时间不传结束时间。排序固定按addtime倒序,紧急上报列表看最新的更有意义,这个细节在需求文档里没写,但实际用起来差别很大。

5. Vue 路由与联调阶段的两个硬骨头

后端的接口好写,真正耗时间的是前端那几个环节:未登录时的跳转、刷新后的状态恢复、打包上线后的界面错位。

5.1 路由守卫与 401 的统一处理

登录态不能只靠路由守卫做,因为 token 过期是服务端说了算。正确姿势是守卫负责拦「没登录」,请求拦截器负责接「登录失效」。

// router/index.js:全局前置守卫,白名单外必须带 token router.beforeEach((to, from, next) => { const token = localStorage.getItem('Token'); const whiteList = ['/login', '/register', '/404']; if (whiteList.includes(to.path)) return next(); if (!token) return next('/login'); // 已登录但菜单没拉过,先补齐动态菜单再放行 if (!store.state.menuLoaded) { store.dispatch('loadMenuByRole', localStorage.getItem('Role')).then(() => next()); } else { next(); } }); // request.js:响应拦截器统一兜 401 service.interceptors.response.use(res => res, error => { if (error.response && error.response.status === 401) { localStorage.clear(); router.replace('/login'); } return Promise.reject(error); });

要点在第二段:loadMenuByRole只做一次,把角色对应的菜单树缓存进 store,否则每次切路由都发一次请求。401 处理里必须localStorage.clear()而不是只删 token,否则 role 残留会让下一次登录跳过角色选择,登进去出现越权菜单。

5.2 打包后布局错乱与样式隔离

本地跑得好好的,npm run build之后表格列宽塌陷、弹窗偏移,八成是两个原因:一是行内 style 用了px硬编码,容器宽度在构建后受 flex 布局影响变了;二是组件里的<style>没加scoped,两个页面同名 class 互相覆盖。做法是给每个视图组件的样式都加scoped,确实需要穿透的用:deep(),并且把弹窗、表格的列宽改成百分比或min-width。改完之后本地复现这个问题也简单:npm run build后用npx serve dist起静态服务,别直接双击index.htmlfile://协议下路由模式会导致白屏。

5.3 上线前的验证清单

联调收尾按固定顺序走一遍,比东一榔头西一棒子省时间:先用三个角色各登一次,确认菜单树和权限矩阵对得上;再用调度员账号新增一条公交调度,切到公交员账号看是否只能查看不能改;然后公交员提交一条紧急上报,管理员审核后调度员生成紧急调度,确认状态字段逐级变化;最后把 token 表的expiratedtime手工改到过去,验证 401 跳转是否干净。这套流程跑通,系统的三条主线基本上就立住了。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 15:15:23

ASP物流管理系统毕业设计:三层架构、数据库与IIS部署全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 15:15:10

谷歌Home接入MCP:AI智能体可控制你的家

编者按&#xff1a;智能家居行业喊了多年的「AI 管家」&#xff0c;正在以一种更务实的方式落地——不是再造一个更聪明的语音助手&#xff0c;而是干脆把家里的设备控制权&#xff0c;交给用户自己选择的 AI。 谷歌 Home 向第三方 AI 智能体开门 据 TechCrunch 报道&#xff0…

作者头像 李华
网站建设 2026/9/17 15:13:09

Flask+MySQL+Tkinter实战:轻量级租房系统架构与数据一致性设计

简介&#xff1a;本资源是一份面向Python初学者与中级开发者的出租房管理系统实战项目文档&#xff0c;适用于计算机专业学生、物业系统开发者及全栈学习者&#xff0c;解决中小型租赁场景下的信息化管理难题。文档以Flask后端架构与Tkinter桌面GUI为核心&#xff0c;完整覆盖数…

作者头像 李华