基于SpringBoot和Vue的工会管理系统设计与实现全解析
1. 动手编码前先把工会业务拆成一张功能图谱1.1 工会管理系统区别于普通增删改查的三个特征每年到三四月份都会有一批人带着同一个选题找到我——基于SpringBoot和Vue的工会管理系统。这个题在计算机毕业设计里属于典型业务管理系统技术上不稀奇但恰恰因为“不稀奇”很多人在答辩时反而拿不出东西功能看着不少真演示起来要么登录弹不出token要么活动报名数据对不上要么表格一翻页就报错。问题根源不是不会写代码而是从需求到数据库再到前后端接口整条链路没有串起来。工会管理系统和学生选课、图书借阅这类毕设选题最大的不同在于它的业务对象是“人”以及人和组织的关系。以学校工会为例用户角色至少有三层普通会员、部门分工会管理员、校工会管理员。系统要管的不只是会员基础信息还包括会费缴纳记录、工会活动发布与报名、困难职工帮扶申请、节日慰问品发放。这些业务可以抽象成“会员—活动—缴费—帮扶”四条主线彼此之间又有交叉活动报名要关联会员缴费记录要关联会员和年份帮扶申请要关联会员和审核人。这是第一个特征业务主线和角色权限交织后台菜单不能简单地按“增删改查”铺开必须先梳理权限边界。第二个特征是流程性业务占比较高。比如困难帮扶申请普通会员提交以后要经过分工会初审、校工会复核、最终公示状态字段至少要设计成“待审核/初审通过/已拨款/已驳回”几种。做这种状态流转比单纯CRUD麻烦但也是评阅老师最看重的地方因为它体现了你有没有“业务流程”意识。第三个特征是统计需求几乎是标配。年终要统计会员人数、会费收缴率、活动参与率、帮扶资金使用情况前端通常需要柱状图和饼图。不要小看这几个统计页面很多同学做到最后才想起来结果只能临时用图表库堆一个写死数据的图表糊弄答辩老师一问“这个数字是从哪张表算出来的”就露馅了。1.2 面向毕业设计的模块取舍哪些功能必须做哪些可以砍毕设时间通常只有三到四个月不可能把真实工会的OA系统完整做出来。按我辅导项目的经验核心功能保留六个模块就足够撑起一场不错的答辩会员管理基础信息维护、分部门检索、导出Excel会费管理按年度缴费、欠费提醒、缴费记录查询工会活动活动发布、报名/取消报名、活动名单导出帮扶管理申请提交、两级审核、状态流转系统管理用户登录、角色权限、菜单管理数据统计会员结构、会费收缴情况、活动参与情况。可以砍掉或弱化的功能包括内部公文流转、邮件通知、工资代扣对接、复杂审批流和消息队列。原因很现实功能越多表越多接口越多文档量成倍上涨最后往往是到处埋雷。模块取舍的逻辑是先保证主线闭环会员能登录、能看到活动、能报名再保证管理闭环管理员能发活动、能审核、能看到统计数据两头都通了系统在演示时就不会出现“点哪个都行但点哪个都不完整”的尴尬。功能图谱不用画得很复杂一张Excel表格列出模块、子功能、角色权限、对应页面就够用这个表后面可以直接变成论文里的功能结构图。2. 后端SpringBoot侧的核心设计从Controller到权限控制2.1 项目骨架与分层为什么我建议按业务模块分包很多教程会把SpringBoot项目按“controller / service / mapper”三层包来组织这种分层没有错但放到工会管理系统这种模块边界清晰的项目里更好的方式是“按业务模块分包模块内部再分层”。我常用的项目结构是这样也是我带过的项目里翻车率最低的一种com.example.union ├── config # 配置类跨域、拦截器、MyBatisPlus配置 ├── controller # 按模块继续分包member、activity、fee、support ├── service # 业务接口与实现 ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体 ├── dto # 前端请求/响应对象 ├── vo # 视图对象比如统计报表的返回结构 ├── common # 统一返回体、异常处理、工具类 └── security # JWT过滤、权限注解为什么要这样分因为当你做到帮扶审核的时候需要同时修改申请状态、写审核日志、更新会员帮扶次数如果全部堆在service里代码会迅速膨胀到难以维护。按业务模块分包每个模块的职责边界是清晰的后期改需求时只动一个包风险可控。实体类也不要追求“一张表一个类”就完了比如登录接口需要的用户信息和会员列表需要展示的部门名称往往要对实体类做裁剪用VO去承接而不是直接把数据库实体序列化返回给前端。2.2 登录与权限JWT还是Session毕设该怎么选工会管理系统的权限模型并不复杂角色就三种管理员、分工会负责人、普通会员。Spring Security JWT是最常见的组合但对毕设来说全量引入Spring Security的学习成本偏高配置类经常会把你绕晕。我的建议是二选一方案A推荐拦截器 JWT。自己写一个HandlerInterceptor或者OncePerRequestFilter校验请求头里的Authorization从token里解析出userId和role再配合自定义的RequireRole注解做权限判断。代码量在八十到一百五十行之间完全可控答辩时还能讲清楚JWT的组成部分和校验流程。方案BSpring Security Sa-Token。如果你对Spring Security配置熟练用Sa-Token会更舒服它把登录、权限、踢人下线都封装好了一个StpUtil.login(userId)就能完成会话创建。缺点是答辩时老师追问底层原理如果你说不清楚反而扣分。实际写JWT工具类并不复杂核心生成代码大致是这样public String generateToken(Long userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 2)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }拦截器里要做的事情很简单放行登录接口其余接口校验token是否存在、能否解析、用户是否有效。解析失败统一返回401前端拿到401就跳转登录页。注意token过期时间设置在1到2小时比较合适太短会让演示中途掉线太长又显得不专业2小时是一个合理的折中。密码存储不要用明文用BCrypt加密这是答辩时几乎必问的安全点。2.3 接口设计规范给前端一个不吵架的约定前后端联调中最容易撕的地方就是返回结构不统一。有的接口返回{code,message,data}有的接口直接返回一个数组有的错误提示用字符串有的用对象前端每接一个接口就要单独写判断逻辑。我的习惯是统一封装一个Result类public class ResultT { private Integer code; // 200成功500业务错误401未登录 private String message; private T data; }Controller基本只返回Result业务异常通过全局异常处理器统一捕获后转成Result。前端axios拦截器里统一判断code等于把错误处理收敛到两个文件里后端一个封装类前端一个request.js。这个约定看似简单却能减少联调阶段一半的返工。接口命名也要有一点强迫症。模块前缀 资源名 动作比如POST /api/member/addDELETE /api/member/{id}GET /api/fee/statistics。不要出现/memberDelete这种把动作揉进路径的写法因为一旦前端要做权限校验和菜单映射不规范的路径会让后端维护者头大。分页接口老老实实用pageNum和pageSize两个参数返回时分页和列表分开前端表格组件才能直接接得住。3. 前端Vue实战组件、路由与接口对接的完整闭环3.1 Vue项目初始化与目录规划别用默认模板直接开写Vue 2还是Vue 3如果后台管理端不用复杂图表两者都够用。考虑到现在新项目优先用Vite Vue 3 Element Plus我建议按这个组合走Vue 3对应的生态更新表格组件、表单校验、对话框的体验都更顺滑而且Element Plus的文档是全的遇到问题搜索成本低。初始化命令很简单npm create vitelatest union-web -- --template vue cd union-web npm install axios vue-router pinia element-plus目录规划的核心原则是views按后端模块一一对应不搞混。我通常这样组织src ├── api # 每个模块一个js文件比如member.js、activity.js ├── router # 路由表 ├── stores # pinia状态管理存用户信息和token ├── views │ ├── login │ ├── layout # 后台主体布局左侧菜单顶部栏 │ ├── member │ ├── activity │ ├── fee │ └── support ├── components # 公共组件分页、上传、详情弹窗等 └── utils # request.js、工具函数目录和命名统一带来的好处是后端接口改了前端知道去哪里改导师抽查代码也能一眼看懂你的项目结构这是隐性加分项。不要把所有页面堆在views根目录下二十几个文件摊在一起截图放进论文都显得项目很乱。3.2 axios封装与请求拦截一个文件解决所有接口烦恼request.js是前后端对接的枢纽。它会做三件事统一baseURL、自动携带token、统一处理返回码。核心代码可以这样写import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use(response { const res response.data if (res.code 200) { return res.data } if (res.code 401) { router.push(/login) } ElMessage.error(res.message || 请求失败) return Promise.reject(res) })注意这里统一返回res.data意味着业务代码里再也不用写response.data.data接口函数非常干净。Vite还需要配置开发服务器代理把/api转发到后端8080端口否则会出现跨域问题这个下面排错章节会细说。前端每个模块的接口文件对应后端的Controller一个文件里集中放三五个接口函数管理起来一目了然。3.3 路由守卫与菜单权限从登录页到首页的链路路由守卫主要是解决“没登录不能进后台”和“角色不同看的菜单不同”两个问题。前者用全局前置守卫判断token是否存在后者在生成菜单时根据角色过滤。前置守卫代码router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })角色菜单权限不建议做太复杂。一个小技巧后端登录接口返回用户角色前端在layout的菜单渲染时按角色过滤不需要在路由表里写几十个动态路由。工会系统角色就三种这种“简单过滤方案”比动态路由更容易讲清楚答辩时老师问你“不同角色怎么控制菜单”你可以直接打开代码演示一个if判断简洁有力。菜单过滤实现时在路由配置里给每个路由加meta比如meta: { roles: [ROLE_ADMIN, ROLE_MANAGER] }菜单渲染组件里读取用户当前角色过滤不匹配的路由项。这个方案还有一个好处页面刷新之后菜单不会消失因为用户信息存在localStorage或者pinia里刷新后重新拿一次就行不用依赖后端动态下发路由。4. 数据库设计才是工会系统的灵魂表结构背后的业务逻辑4.1 核心业务表会员、活动、会费、帮扶四张主表怎么设计很多毕设系统最后的数据库也就七八张表工会管理系统如果做得好十到十五张表比较合理。我一般按如下方式设计核心表t_member会员表id、member_no工号/学号、name、gender、dept_id部门、phone、email、join_date入会时间、status是否在职、password、role_idt_activity活动表id、title、content、activity_date、location、max_count、status未开始/进行中/已结束、create_by、create_timet_activity_signup活动报名表id、activity_id、member_id、signup_time、status已报名/已取消t_fee_record会费缴纳记录表id、member_id、year、amount、pay_status已缴/未缴/减免、pay_timet_support_apply帮扶申请表id、member_id、type大病/困难/其他、reason、apply_time、status、reviewer_id、review_comment、review_time。为什么要单独建t_activity_signup而不是在活动表里加一个participants字段因为一个活动对应多个会员一个会员可以参加多个活动这是典型的多对多关系。如果图省事在活动表里存逗号分隔的会员ID后面统计活动参与率时会痛苦到怀疑人生。这个点在我的经验里是答辩老师最爱问的问题之一答得好印象分会明显不同。4.2 一对多和多对多场景在MySQL里的落地写法会员和部门是一对多活动和会员是多对多。前者只需要在会员表里存dept_id外键后者需要单独建关联表。建表SQL示例简化CREATE TABLE t_member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_no VARCHAR(30) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, dept_id BIGINT, status TINYINT DEFAULT 1, PRIMARY KEY (id) ); CREATE TABLE t_activity_signup ( id BIGINT PRIMARY KEY AUTO_INCREMENT, activity_id BIGINT NOT NULL, member_id BIGINT NOT NULL, signup_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_activity_member (activity_id, member_id) );注意这里有个关键约束uk_activity_member。它的作用是防止同一会员重复报名同一活动。这是我在实际项目里经常看到漏掉的细节——没有唯一约束用户疯狂点击报名按钮就会出现重复数据接口层再校验也不够稳。索引也很值得讲t_fee_record表联合索引(member_id, year)t_activity_signup表索引(activity_id)这些索引能在列表查询和统计时明显提速也让导师看到你的数据库功底。设计表时每张表都加上create_time、update_time两个通用字段MyBatis-Plus可以自动填充这在很多企业项目里都是标准习惯写进论文也是加分项。4.3 初始化数据的坑没有测试数据功能演示寸步难行数据库脚本和初始化数据的交付是毕设里最容易被忽视、又最能决定演示成败的部分。我的建议是写一个data.sql或直接在sql脚本末尾插入充足的测试数据会员至少20条覆盖3个以上部门男女比例、不同入会年份都要有活动至少5条包含“未开始/进行中/已结束”三种状态缴费记录覆盖近3年故意制造几条“未缴”记录让欠费提醒和统计图表有意义帮扶申请至少2条分别停留在不同审核状态。为什么要这样做因为演示时的核心诉求是“所有状态都有数据、所有按钮都能点出效果”。很多同学表结构建得很漂亮一查数据库全是空的接口自然返回空列表图表画不出来答辩效果大打折扣。记住测试数据也是一种产品设计要按“演示故事线”来准备而不是随手insert几条ABC。5. 毕设交付三件套源码、数据库脚本和使用文档的规范化5.1 源码目录整理导师查重之前先看的是结构导师或评阅老师拿到项目文件后第一件事不是跑代码而是解压后看目录。如果根目录下target、node_modules、.idea、dist这些生成目录全都上传了印象分会立刻受损。交付源码包时根目录建议这样组织union-management-system/ ├── backend/ # SpringBoot后端 │ ├── src/ │ ├── pom.xml │ └── README.md ├── frontend/ # Vue前端 │ ├── src/ │ ├── package.json │ └── README.md ├── docs/ # 论文、开题、答辩PPT ├── sql/ # 数据库脚本 └── README.md # 项目整体说明.gitignore文件要提前配置排除target、node_modules、.idea、dist、*.log。不要上传本机的IDE配置文件和个人密钥。后端application.yml中不要把数据库密码写成你本机的真实密码尽量统一成root/123456并在文档里说明避免别人拿到项目连不上数据库直接判死。README里写清楚JDK版本、Node版本、MySQL版本、启动步骤这些信息看起来琐碎却是你项目是否“可用”的第一道证明。5.2 数据库脚本的交付格式不要只给一个dump文件数据库脚本最好拆成两个文件01_create.sql建库建表语句包含表结构注释每个字段都有COMMENT02_init_data.sql初始化数据脚本包含必要的测试数据和初始账号。好处有两个。第一导师在教学环境里重新部署时可以相对清晰地复现你的环境第二论文中“数据库设计”章节可以直接引用01_create.sql中的表结构。相比之下一个几百行的mysqldump文件包含了版本信息、临时表、锁表语句既不美观也不便于教学复现。初始账号要写在README里比如管理员账号 admin / admin123角色ROLE_ADMIN分工会负责人 manager / manager123角色ROLE_MANAGER普通会员 user / user123角色ROLE_USER。账号角色要覆盖全否则评审老师登录后看到的功能和普通会员一样会质疑你的权限设计。密码用BCrypt加密后的字符串放进脚本并在文档里注明明文避免老师无法登录。5.3 文档该怎么写从需求分析到测试用例的套路毕设文档虽然是“文档”但最能拉开差距的地方是逻辑闭环。很多论文写了完整的需求分析系统设计却对不上写了系统设计测试用例又是网上抄的。我的建议是把三个东西严格对应需求分析里列出的每个功能点在系统设计里要有对应的模块和表系统设计里的每张表在数据库脚本里要能一一找到论文里的每个测试用例要用真实运行的截图和SQL结果佐证。具体到测试章节不用追求几十个用例写十到十五个覆盖核心链路的用例就够了比如会员登录成功/密码错误、管理员新增会员、普通会员报名活动、管理员审核帮扶申请、导出活动报名名单。每个用例写明前置条件、操作步骤、预期结果、实际结果并配上截图。这四件套做完评阅老师基本挑不出大毛病。论文里的流程图、用例图、ER图直接从绘图软件里导出矢量图不要用手绘截图清晰度会直接影响观感。6. 从零到演示成功的踩坑实录一次完整排错复盘6.1 环境不统一引发的连环报错JDK、Node、MySQL版本这个案例是我实际帮一个学弟排过的他前端npm run dev正常后端IDEA也能启动但一登录就报错前端控制台显示500。查日志发现最底层错误是Caused by: java.sql.SQLSyntaxErrorException: Unknown column dept_name in field list。MyBatis-Plus自动生成的SQL里带了dept_name但他本地的t_member表根本没有这个字段。根因很有意思他的实体类Member里定义了private String deptNameMyBatis-Plus默认开启驼峰映射查询时会把dept_name拼进SQL但表里字段却是dept_id。所以表结构和实体类字段不一致是这类项目最常见的隐性bug。解决办法是统一要么实体类不写冗余字段用DTO单独承接查询展示字段要么表里就加上dept_name冗余列。我更建议用DTO方案因为表结构越干净越好扩展。这个问题也提醒一件事开工前先统一三样东西的版本——JDK建议8或11别图新上17除非你对兼容性有把握、NodeVue3建议16以上、MySQL5.7或8.0注意8.0的密码校验规则和驱动版本。版本不一样很多问题根本复现不出来排错会浪费大量时间。6.2 端口占用与跨域问题最常被卡住的两道坎后端8080端口被占用是另一个高频现场。症状是IDEA启动报Port 8080 was already in use原因多数是之前启动的后端进程没关干净。解决方式并不是改spring端口而是找到占用进程netstat -ano | findstr 8080然后kill对应的PID。如果前端页面上看到Access to XMLHttpRequest has been blocked by CORS policy首先检查Vite的proxy配置使用开发代理转发前后端配合时尽量避免以后端加CrossOrigin的方式解决。因为加CrossOrigin虽然能过浏览器这一关但每次请求会多一次OPTIONS预检而且生产部署时还要把方案改成Nginx反向代理不如从一开始就统一用代理方案。Vite的代理配置很简单server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端代码里的请求统一走/api开头开发环境交给Vite转发生产环境交给Nginx转发前后端代码都不需要写死IP。这个设计思维在简历上也能写一句“熟悉前后端联调与跨域代理方案”比单纯堆框架名有说服力。6.3 联调阶段的“数据怎么没了”事务和级联删除的锅学弟的帮扶申请模块出现过一个诡异的bug管理员删除一个会员后活动报名记录、缴费记录、帮扶申请记录全没了。这其实是他把外键级联删除配置错了t_member表的外键写了ON DELETE CASCADE把关联子表的记录一并删了。业务系统里删除会员数据本身应该标记为“离职/停用”而不是物理删除即使要删除关联记录也不应该级联清空因为缴费记录和帮扶记录是有审计价值的。更常见的同类问题是删除部门时报“外键约束失败”。这个问题的正确姿势通常是先检查该部门下是否有会员有则提示“该部门下存在N名会员请先转移会员后再删除”。这些细节需要在service层写逻辑而不是依赖数据库的级联行为。不要把外键级联删除当成省事的银弹宁可多写几行查询代码也要保住业务数据的完整性。还有一类坑藏在事务里比如帮扶审核接口既要更新申请状态又要写审核日志还要更新会员的帮扶次数任何一个步骤失败都会导致数据不一致。正确做法是在service方法上加Transactional注意要加在public方法上而且不要在同类的内部方法间互相调用绕过代理然后做一次异常回滚测试故意让最后一步抛错看前面两步是不是真的回滚了。这个测试我建议你在答辩前做因为老师非常喜欢问“你这个操作保证了原子性吗”。最后分享一个我自己的习惯正式演示前一天把数据库脚本从零执行一遍再走一遍核心链路。这一步能暴露八成以上的现场事故。很多毕设不是死在技术难度上而是死在“我以为没问题”上。工会管理系统本身不算难但它涉及的模块、权限、表关系和文档交付恰好覆盖了企业级项目的完整链路。把它做成一个真正能跑、能演示、能讲清楚的系统比做完十张花哨页面更有价值。