☰
基于SpringBoot和Vue的工会管理系统设计与实现全解析
2026/9/30 12:34:53 网站建设 项目流程

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的组成部分和校验流程。

方案B:Spring 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 Result<T> { private Integer code; // 200成功,500业务错误,401未登录 private String message; private T data; }

Controller基本只返回Result,业务异常通过全局异常处理器统一捕获后转成Result。前端axios拦截器里统一判断code,等于把错误处理收敛到两个文件里:后端一个封装类,前端一个request.js。这个约定看似简单,却能减少联调阶段一半的返工。

接口命名也要有一点强迫症。模块前缀 + 资源名 + 动作,比如POST /api/member/add,DELETE /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 vite@latest 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_id;
  • t_activity(活动表):id、title、content、activity_date、location、max_count、status(未开始/进行中/已结束)、create_by、create_time;
  • t_activity_signup(活动报名表):id、activity_id、member_id、signup_time、status(已报名/已取消);
  • t_fee_record(会费缴纳记录表):id、member_id、year、amount、pay_status(已缴/未缴/减免)、pay_time;
  • t_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:建库建表语句,包含表结构注释,每个字段都有COMMENT;
  • 02_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 deptName,MyBatis-Plus默认开启驼峰映射,查询时会把dept_name拼进SQL,但表里字段却是dept_id。所以表结构和实体类字段不一致,是这类项目最常见的隐性bug。解决办法是统一要么实体类不写冗余字段,用DTO单独承接查询展示字段,要么表里就加上dept_name冗余列。我更建议用DTO方案,因为表结构越干净越好扩展。

这个问题也提醒一件事:开工前先统一三样东西的版本——JDK(建议8或11,别图新上17除非你对兼容性有把握)、Node(Vue3建议16以上)、MySQL(5.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方法上,而且不要在同类的内部方法间互相调用绕过代理),然后做一次异常回滚测试:故意让最后一步抛错,看前面两步是不是真的回滚了。这个测试我建议你在答辩前做,因为老师非常喜欢问“你这个操作保证了原子性吗”。

最后分享一个我自己的习惯:正式演示前一天,把数据库脚本从零执行一遍,再走一遍核心链路。这一步能暴露八成以上的现场事故。很多毕设不是死在技术难度上,而是死在“我以为没问题”上。工会管理系统本身不算难,但它涉及的模块、权限、表关系和文档交付,恰好覆盖了企业级项目的完整链路。把它做成一个真正能跑、能演示、能讲清楚的系统,比做完十张花哨页面更有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询