ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue+MyBatis工资系统实战:数据库设计到部署全流程

SpringBoot+Vue+MyBatis工资系统实战:数据库设计到部署全流程 去年秋天帮一个学弟改毕业设计他用SSM写了个工资管理系统页面丑不说动不动就数据库连接超时。我花了两个晚上帮他重构到SpringBootVue顺手整理了一套完整的源码结构。后来好几个朋友找我要这份东西今天干脆把设计思路和实现要点一次性写清楚——这就是这篇文章的来历。这套基于SpringBootVueMySQLMyBatis的工资信息管理系统适合三类人看正在做Java全栈毕业设计的在校生、想快速上手企业级前后端分离开发的自学者、以及需要给公司内部搭建一套轻量工资管理工具的非专业开发人员。它能让你从零跑通“数据库设计→后端接口→前端页面→部署上线”的完整链路而不是停留在跟着教程敲Demo的层面。我做这套系统时有几个明确目标表结构要经得起推敲不能是那种只为了CRUD硬凑出来的权限逻辑要真实财务人员的操作记录必须留痕前后端要真正分离Vue打包后能独立部署代码量控制在合理范围一个认真学的人两周能看完看懂。下面按我实际开发的顺序来讲这样更贴近你从头做一个项目的真实节奏。1. 工资管理系统和普通CRUD Demo的本质区别很多人觉得工资系统无非就是把员工工资加加减减存进数据库真正动手后发现完全不是这么回事。这个项目我之所以推荐作为进阶练手项目恰恰是因为它的业务复杂度恰到好处——比图书管理难但又没到电商那种量级做完以后你对“管理系统”这四个字的理解会彻底改变。1.1 工资计算里藏着的业务复杂度先说工资结构本身。一份真实的工资单至少包含基本工资、岗位工资、绩效奖金、加班费、餐补、交通补贴、考勤扣款、社保个人部分、公积金个人部分、个税这十来个字段。其中社保公积金各地区的缴费比例不一样个税又涉及起征点和累进税率这还不是最麻烦的——麻烦的是不同公司对这些字段的定义千差万别有的公司把绩效算进基本工资有的公司单独列项。从数据库设计的角度这就引出了一个关键决策工资字段是直接做成表的列还是用“工资项金额”的纵向表我做过三个项目最终全部选择了显式字段设计——就是每个工资项占一列。原因很简单工资系统的查询模式高度固定按月查询、按员工查询、按部门汇总横向表结构用一条SQL就能把整张工资单查出来配合MyBatis的ResultMap映射非常直观。纵向表虽然灵活但查一次工资单要GROUP_CONCAT或者多次JOIN写起来费劲性能还差。个税计算这块我直接用BigDecimal封装了一个TaxCalculator工具类。很多人图省事用double算钱这是大忌。double的二进制浮点表示会导致0.10.2不等于0.3这类问题工资算错一分钱在财务那里都是事故。BigDecimal配合setScale(2, RoundingMode.HALF_UP)四舍五入到分这是Java金额计算的铁律。1.2 “管理”二字体现在权限和审计上工资系统不只是算工资它是企业内部敏感度最高的系统之一。财务人员能看到全员工资部门主管只能看到自己部门的普通员工只能看到自己的。这就是基于角色的访问控制RBAC模型我把它具体化为三个角色管理员ADMIN、财务FINANCE、普通员工EMPLOYEE。管理员负责维护员工信息、部门信息、账号分配财务负责工资录入、核算、发放、导入导出普通员工只能查看自己的工资条连别人的月薪总额都不应该出现在列表里。这个权限模型看着简单但我在后端的Service层做了双重校验——不只靠前端隐藏按钮每次查询工资列表时Mapper层自动拼接当前用户的部门ID条件防止有人绕过前端直接调接口。审计这块容易被忽略。工资数据出了争议你得能回答“这笔数据是谁在什么时候改的”。我加了一张操作日志表用Spring的AOP切面统一记录关键操作修改工资、删除工资、导入数据、导出数据都会落日志。这个功能不需要额外引框架一个自定义注解加一个Aspect类几十行代码搞定但整套系统的可信度完全不一样。2. 技术栈选型的思考过程为什么是SpringBootVueMyBatis这套技术栈现在几乎是Java全栈开发的标配但标配不等于没有选型逻辑。我在定方案时做了几组对比把当时的判断依据写出来你参考的时候能更明白每一层为什么是它。2.1 后端SpringBoot的“约定优于配置”和MyBatis的灵活SQL后端框架候选方案无非SpringBoot和SSM。SSM的痛点你写一次配置就懂了——spring-mvc.xml、mybatis-config.xml、web.xml三个配置文件来回折腾光搭环境就能劝退一大半人。SpringBoot用自动配置把这些全部干掉内嵌Tomcat一个Application类启动整个Web应用这对快速搭建和后期维护都是降维打击。持久层我用MyBatis而不是JPA/Hibernate核心原因是工资系统里有大量多表关联查询和月度汇总报表SQL的灵活性太重要了。举个例子查询某个月的工资汇总按部门分组统计应发合计、实发合计、社保合计这种SQL用MyBatis写在XML里看得清清楚楚调优也方便。JPA那种Entity关联查询在这种场景下写起来绕还不容易控制最终生成的SQL。当然单表CURD用MyBatis-Plus更香这个建议你也采纳省下的BaseMapper方法足够你多写两个页面。2.2 前端Vue加Element UI是效率最优解前端选Vue2还是Vue3我纠结了一阵子。如果你的毕设或者项目没有历史包袱直接上Vue3加Element Plus组合式API写起来确实比Vue2的选项式API顺手Vite的构建速度也比Webpack快好几倍。我当时给学弟重构时他项目里还是Vue2迁移成本高才保留Vue2。新写项目我不推荐再开Vue2了。Element UI这套组件库我必须多说一句。你用原生HTMLJS开发过管理系统的话会明白它的价值——一个带搜索、分页、批量操作的Table组件原生写至少要两百行JavaScriptElement UI三行代码搞定而且样式统一、交互规范。加上Axios做HTTP请求Vue Router做路由正好凑齐前后端分离开发的核心拼图。2.3 中间件和工具链的选型数据库我选MySQL 8.0。5.7和8.0的语法绝大部分兼容但8.0的窗口函数对做工资月度同比、部门排名这类报表非常有用既然是新项目就没必要守着老版本。Navicat做可视化工具见仁见智它收费社区版足够用或者用开源的DBeaver功能上完全够。这里需要提醒一个坑MySQL 8.0默认使用caching_sha2_password认证插件老版本的JDBC驱动连接时会报SSL错误或者认证失败。解决办法要么在连接URL加useSSLfalse来跳过SSL验证要么换成mysql-connector-java 8.0以上版本的驱动。我后面部署章节还会详细讲。3. 数据库设计工资业务怎么建模才叫合理数据库是这套系统的地基我花的时间比写代码还多。核心原则只有一条面向查询设计表而不是面向录入设计表。工资系统录入是一次性的查询是反复的设计时优先保证查询的效率和便利。3.1 六张核心表的结构和关联关系我最终落了六张表用户表、员工表、部门表、工资表、工资明细表历史版本、操作日志表。用Navicat建完表之后再在MySQL Workbench里生成ER图看关系方便后期写文档。这里直接把核心表结构贴出来。用户表管理登录账号和员工表一对一关联密码用MD5加盐存储。注意用户名要建唯一索引这是安全底线CREATE TABLE sys_user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(128) NOT NULL COMMENT 密码加盐后MD5, salt varchar(20) NOT NULL COMMENT 盐值, role varchar(20) NOT NULL DEFAULT EMPLOYEE COMMENT 角色, employee_id int DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username), KEY idx_employee (employee_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;员工表存基础人事信息字段包括姓名、工号、部门ID、职位、入职日期、状态。工号不能用自增主键替代因为公司内部流转时工号不变但员工ID可能因为数据整理而变化。部门表简单得多就ID、名称、负责人三个字段。工资表是核心业务表设计时用了唯一联合索引CREATE TABLE salary ( id int NOT NULL AUTO_INCREMENT, employee_id int NOT NULL COMMENT 员工ID, salary_month varchar(7) NOT NULL COMMENT 工资月份如2024-05, base_salary decimal(10,2) NOT NULL COMMENT 基本工资, post_salary decimal(10,2) NOT NULL COMMENT 岗位工资, perf_salary decimal(10,2) NOT NULL COMMENT 绩效工资, overtime_salary decimal(10,2) DEFAULT 0.00, meal_allowance decimal(10,2) DEFAULT 0.00, transport_allowance decimal(10,2) DEFAULT 0.00, attendance_deduction decimal(10,2) DEFAULT 0.00, social_insurance decimal(10,2) NOT NULL COMMENT 社保个人部分, housing_fund decimal(10,2) NOT NULL COMMENT 公积金个人部分, tax decimal(10,2) DEFAULT 0.00 COMMENT 个税, gross_salary decimal(10,2) NOT NULL COMMENT 应发合计, net_salary decimal(10,2) NOT NULL COMMENT 实发工资, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_emp_month (employee_id, salary_month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 为什么工资明细要单独做一张历史表工资数据一个显著特点是“发了就不能改”——至少正式发出去的版本得保留原样。如果财务核算出错需要调整正确的做法不是UPDATE原记录而是生成一条修正记录保留历史。所以工资明细表的设计思路是每月首次核算时插入初始版本后续每次修正插入新版本通过版本号区分。实际开发中这个需求经常被简化掉很多毕设就一张工资表改了就改了。如果系统只是课堂作业无所谓但你想让项目在企业内部真正跑起来这条设计是关键的加分项。我的做法是加了一个salary_history表核心字段和salary表一致额外加version、change_reason、operator_id三个字段。按月份员工查询时优先取最高版本。3.3 工资汇总报表的一条SQL部门月度汇总报表是财务最常用的功能。左连接员工表拿部门信息按部门和月份分组聚合一条SQL完成SELECT d.name AS dept_name, s.salary_month, COUNT(s.id) AS person_count, SUM(s.gross_salary) AS total_gross, SUM(s.social_insurance) AS total_social, SUM(s.housing_fund) AS total_fund, SUM(s.tax) AS total_tax, SUM(s.net_salary) AS total_net FROM salary s LEFT JOIN employee e ON s.employee_id e.id LEFT JOIN department d ON e.department_id d.id WHERE s.salary_month #{month} GROUP BY d.id, s.salary_month这条SQL走的是联合索引uk_emp_month当月数据量在几百条的情况下毫秒级返回。后续如果公司规模扩大可以考虑按月份做分区表但这是后话项目初期不建议过度设计。4. 后端实现要点分层、鉴权、导入导出后端我用标准的Controller-Service-Mapper三层架构包结构按照com.example.salary来组织下面分controller、service、mapper、entity、common、config、aspect几个子包。分层清晰是老生常谈但它直接决定你排bug的效率一个请求从Controller进来Service处理业务逻辑Mapper操作数据库每一层只做自己的事出问题能立刻锁定层次。4.1 JWT令牌加拦截器Spring Security放到什么时候用登录鉴权的方案我选的是JWT加拦截器而不是直接上Spring Security。原因非常现实Spring Security的学习曲线陡峭配置不当会出现各种诡异的过滤链问题对于一个核心需求清晰的业务系统来说它的80%功能你都用不到。JWT的方案更透明——登录成功发一个带过期时间的Token后续每个请求都在拦截器里校验Token这个逻辑你能完全掌控。放行逻辑必须在拦截器里配置清楚。登录接口、静态资源要放行工资相关接口全部拦截同时把预请求OPTIONS放行否则前端跨域请求会二次预检。下面这段是我拦截器的核心写法public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\登录已过期请重新登录\}); return false; } } }密码存储的处理也给个建议不要明文存不要只做一次MD5。我的方案是每个用户随机生成一个盐值salt加密码做MD5这样就算数据库泄露彩虹表也解不出原始密码。MD5在现代安全标准下不够强但配合随机盐在中小型系统里是成本和安全的平衡点。4.2 工资导入导出EasyExcel省下两个通宵工资数据的录入如果靠表单一页页填一个月几十上百人就得填到崩溃。所以批量Excel导入是刚需。POI和EasyExcel我相信你都听说过我的建议是直接用EasyExcel没有悬念。EasyExcel是阿里开源的底层还是POI但把大文件处理的OOM问题解决了。它最重要的优势是API设计极简读一行回调一行不像原生POI要自己遍历Row和Cell。写一个导出工具也很方便用注解标注实体类字段即可ExcelProperty(value 工号, index 0) private String employeeNo; ExcelProperty(value 姓名, index 1) private String employeeName; ExcelProperty(value 基本工资, index 2) private BigDecimal baseSalary;实际开发中导出工资条还有一个细节模板文件里可能包含公式或者合并单元格EasyExcel对这类模板写操作不如POI灵活。如果只是数据列表导出表头加数据行EasyExcel完胜如果要输出复杂版式才需要用POI手写Workbook。我项目里做的是工资条导出用EasyExcel足够每个员工一张工资单区块做不到我用的是一行一个员工的数据导出版本财务拿Excel再套打。导入的校验环节说了真金白银的经验。Excel里的数据不能直接入库至少三道关卡格式校验——金额列必须能转成BigDecimal逻辑校验——社保不能大于工资唯一性校验——同一员工同一月份不能重复导入。有一行数据不合法我默认全批回滚不要出现部分成功部分失败的情况。否则财务拿着半截数据去发工资账目对不上就是事故。4.3 Service层的事务边界划分工资计算的整个流程涉及多个表的更新事务边界必须清晰。Spring的Transactional注解默认只回滚RuntimeException这点容易踩坑。如果事务方法抛出的是检查异常或者你自己catch住了异常没往上抛事务是不会回滚的。我的处理原则是事务内不能吞异常。Service层做工资核算时先算应发合计和实发合计再插入明细表再写日志表任何一个环节失败都要让整个操作回滚保证不出现半张工资单。Controller层负责捕获Service抛出的业务异常转成统一的JSON返回给前端。业务异常的基类我命名BusinessException在全局异常处理器里统一拦截返回格式形如{code:500, msg:社保字段不能为空}。5. 前端页面实现开发顺序和权限控制的完整链路Vue前端这部分我按实际开发顺序来讲。先搭骨架、再写登录、最后填充业务页面每一步解决什么问题说清楚。5.1 Vite创建项目到Axios封装的完整链路创建项目这步现在用Vite已经是共识了一句命令即可npm create vitelatest salary-web -- --template vue但有个细节我踩过坑Vite默认创建的项目没有加路由依赖也没有配路径别名。你要自己装vue-router和axios并改vite.config.js。路径别名长这样resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }不配的话你import组件时只能用相对路径一层层往上跳一旦目录层次深改起路径来想死的心都有。Axios封装要解决的核心问题是统一处理Token和错误响应。请求拦截器里从localStorage取Token加进Header响应拦截器里判断HTTP状态码401就清空登录状态并跳回登录页500就弹出后端返回的错误信息。这套逻辑在任何项目里都是直接挪用的一次封装终身受益。跨域问题如果你前端口口声声说要“解决”其实开发模式下最简单的方案就是Vite的proxy代理把/api前缀的请求转发到后端8080端口生产环境部署时再交给Nginx处理同源策略。在vite.config.js里这样配server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }5.2 登录页、路由守卫和动态菜单登录页的逻辑很简单把表单数据POST到后端登录接口拿回Token存进localStorage跳转到首页。但你要理解一个现代前后端分离系统的本质——登录页只是入口真正的防线在路由守卫。路由守卫写在router/index.js里用router.beforeEach钩子判断如果目标路由需要认证而本地没有Token跳登录页如果已经登录而访问的是登录页跳首页如果已登录但角色不匹配提示无权限。这个守卫生效后用户绕过前端直接改地址栏访问财务页面就做不到了——当然后端接口的权限校验才是最后的兜底前端路由守卫只是用户体验层面的双层保障。菜单我坚持动态渲染后端登录后返回角色前端根据角色动态生成侧边栏菜单。管理员能看到系统管理菜单财务能看到工资管理菜单普通员工只能看到我的工资菜单。这个动态菜单逻辑一开始就写好后面加页面只需要在路由表加配置不需要动菜单代码。5.3 工资列表页的开发顺序工资列表页是系统的核心页面开发顺序我建议从后往前倒着做先确定表格要展示的列再确定筛选条件再写表单弹窗最后处理分页。总列数别超过12列太多就横向滚动了财务操作起来也不方便。查询区我用三个筛选条件就够了月份一个月份选择器、部门下拉选、员工姓名或工号模糊输入。查询按钮触发列表重新加载重置按钮清空条件。工资的录入和编辑用同一个Dialog弹窗表单Item根据是否编辑状态决定是否禁用——编辑已有工资时员工和月份不允许改这是财务逻辑上的红线防止把5月的工资改到6月去。分页组件我用的是Element Plus的Pagination注意两个关键事件current-change和size-change都要触发重新查询而且查询条件变了要手动把页码重置到第一页。这个细节看似微小但实战中特别影响体验。5.4 ECharts报表让工资数据“看得见”报表功能是整个项目最亮眼的部分也是答辩时加分最明显的地方。我用ECharts做了两个图部门平均工资柱状图、近半年工资总额趋势折线图。柱状图的数据来源是后端汇总接口返回部门名和平均实发工资。折线图的数据来源是另一个汇总接口按月份聚合实发工资总额。Vue里使用ECharts的方式很简单先npm install echarts然后在组件里导入并用ref绑定DOM容器setOption填充配置。这里核心心得是一个组件里别写死图表配置一个页面多图时按图表拆子组件数据和配置通过props传入这样代码可维护性高很多。6. 部署上线从打包到跑通的完整链路与排错实录系统开发完不是终点跑不起来一切都白搭。我部署时踩了一堆坑按“打包→静态资源→数据库连接→启动”的顺序整理出来。6.1 前端打包和后端jar包的配合方式前后端分离项目生产环境有两种部署方案第一种是把前端构建产物放到后端的static目录下后端直接托管静态页面打成一个jar包发布。这种方案小规模内网系统完全够用部署最省事一个java -jar命令全搞定。第二种是前端独立部署到Nginx后端jar包单独跑Nginx配置反向代理把/api请求转发到后端端口。这是更标准的实践也是我在项目中推荐的方式理由有二前端页面更新只需替换静态文件不用重启后端服务Nginx处理静态文件性能远优于Tomcat还能顺带做请求日志、负载均衡。我的建议是如果你在本地测试或者答辩演示用第一种。如果你想要完整的企业级部署经验用Nginx方案。两个方案都跑通一遍等于把运维知识也温习了一次。6.2 我踩过的MySQL连接坑和解决办法部署时数据库连接报错是最常见的搜热词里“mysql ssl连接错误”出现频率很高。原因我之前提过——MySQL 8.0的默认认证插件是caching_sha2_password而应用连接的JDBC驱动版本如果太老协商协议不匹配直接报错。解决方式是在application.yml的数据库连接串上配置useSSLfalse和allowPublicKeyRetrievaltrue。后者的作用是允许客户端在需要使用公钥来传输密码时从服务器请求公钥。这个配置在本地开发时可能没暴露问题一部署到服务器就炸因为本机MySQL版本可能和服务器不一样。还有一个常见错误是连接串里的timezone问题。数据库和服务器的系统时区不一致时插入时间字段会差8个小时。解决方案是在JDBC URL显式指定serverTimezoneAsia/Shanghai前端显示的时间就永远对齐北京时间了。6.3 端口被占用与会话失效的排查java -jar启动时最常见的错误就是8080端口被占用。Linux下先netstat -tunlp | grep 8080找进程号kill掉就好。Windows下netstat -ano | findstr 8080查PID再进任务管理器结束进程。这个排查步骤几乎每个项目都会遇到记牢即可。会话失效的问题比较隐蔽。JWT的过期时间我设的是8小时但用户操作中途超过8小时再点按钮请求被拦截返回401。前端响应拦截器捕获401后不只是跳登录页还要把localStorage清干净否则用户登录不了还会报旧Token的错误。我当时排查这个花了一下午最后发现是响应拦截器里没清旧Token换新Token和新旧数据混在一起导致的。6.4 上线前的自测清单最后给你一份我每次上线前都要过的自测清单照着检查一遍能避免大多数尴尬问题管理员能否正常新增员工并分配账号财务能否导入一份带错误数据的Excel验证回滚机制普通员工登录后能否看到修改工资的按钮应该看不到修改工资后操作日志表是否追加了记录导出Excel后重新导入是否正常强制刷新浏览器再操作是否出现Token失效报错重启后端服务前端页面是否仍能正常打开最后说一点个人体会。这套系统做完之后我没让它停在毕业设计或者Demo的阶段而是真的部署到公司内网用了三个多月。期间财务提了几次需求比如批量修改岗位工资、工资条按部门分批查看、复盘上个月的漏发补发记录这些需求最后都落成了新表和接口。我最大的感受就是管理系统的价值不在于代码写得有多“高级”而在于它能不能真正贴合业务流转的每一步。你照着这套设计做出自己的版本后建议也把源码留好后续加功能时你会庆幸当初表结构留了足够的设计空间。
返回列表