
拿Java SpringBootVue3MyBatis 工资信息管理系统源码前后端分离MySQL数据库这个标题来说我第一反应就是——这不是那种看着高大上但其实空壳的营销文案而是一个真正值得拆一遍的经典全栈实操项目。工资管理系统在Java生态里几乎就是一套标准套餐用户权限、员工档案、薪资计算、数据导入导出、报表统计CRUD为主但又不只是CRUD正好把SpringBoot、MyBatis、Vue3、MySQL这些核心技能点全部串起来。不管你是准备毕业设计、正在找工作想做项目经验还是小团队里需要一个内部人事工资工具这套系统的架构和代码思路都能直接拿过去用。接下来我按自己做这类项目的习惯把这个系统的技术选型、数据模型、前后端实现、部署踩坑整个拆一遍。中间会穿插不少我在真实开发里遇到过的细节问题内容偏实战跟着走一遍你基本能独立复现。1. 项目定位为什么是工资管理系统1.1 这类系统为什么经久不衰工资信息管理是企业管理里最刚需的场景之一。每个公司不管规模大小都要有员工档案和工资发放记录小公司用Excel也能凑合但只要人数超过几十个Excel就会变得非常痛苦发薪公式不统一、修改没有留痕、员工问工资明细没法快速回复、年终统计要几个小时翻文件。所以业务侧的核心痛点就四个工资计算要准确、工资数据要保密、历史记录要可追溯、统计查询要高效。一个工资管理系统本质上就是围绕这四个点把流程线上化。也正因为需求清晰且边界明确它成为无数Java开发者练手、毕设和内部工具的首选项目——你不需要理解复杂的业务模型就能把一套前后端分离的应用完整做出来。1.2 技术栈选型背后的逻辑这个标题的技术栈组合不是随手定的每一层都有它被选中的理由SpringBoot目前Java后端的事实标准内嵌Tomcat、自动配置、生态齐全不需要你手动去配一堆XML。它解决了让Java Web开发变简单这个问题新手也能快速搭出可运行的REST API。Vue3 前后端分离Vue3的Composition API让组件逻辑复用变得很顺畅配合Vite开发体验极好。前后端分离意味着后端只出接口前端只管渲染两端可以并行开发也能各自独立部署这也是目前绝大多数企业Web项目的形态。MyBatis相比JPA的全自动ORMMyBatis的半自动特性让你对SQL有完全控制权。工资系统里经常出现复杂统计SQL比如多表联查、条件动态拼接、分组汇总MyBatis在这种场景下写出明确SQL的效率远超JPA的抽象查询。MySQL免费、稳定、文档多、资料多中小型系统的数据库首选。配合Navicat或DataGrip这种可视化工具设计表结构非常顺手。这套组合最大的优势是招人容易、资料多、排错不难。遇到问题随便一搜就是前人踩过的坑对一个需要长期维护的项目来说这个生态成熟度的加分项比某个技术栈本身多先进更重要。1.3 模块清单从标题倒推功能边界原始标题没有给出完整功能说明但以工资管理系统这个业务名称为基础一名合格的产品经理或架构师会拆分出以下核心模块模块核心功能对应角色登录认证账号密码登录、Token签发、退出登录所有用户员工管理员工档案增删改查、部门分配、离职状态管理员、人事部门管理部门树维护、部门负责人设置管理员工资管理工资单录入、批量调整、工资条生成、个税与扣款计算财务、管理员统计报表月度工资汇总、部门均薪、趋势图管理员、财务系统管理用户角色管理、操作日志、数据字典管理员员工角色通常只拥有查看本人工资条的权限这是工资系统区别于普通业务系统的地方——敏感数据必须靠角色隔离。后续的权限设计也围绕这一点展开。2. 数据库设计与数据模型2.1 设计思路从业务概念到表结构任何系统动手写代码前先画数据模型。工资系统的核心概念就四类组织部门、人员工、钱工资单、账号系统用户。它们的关系也不复杂一个部门下有多个员工一个员工在每个月产生一条工资记录一个系统账号绑定一个员工身份。设计时有几个关键决策员工表与系统用户表分开。员工是HR视角的档案数据系统用户是IT视角的登录凭证二者通过employee_id关联。原因很简单不是所有员工都需要登录系统比如产线工人可能只看工资条由主管代为查询也不是所有能登录系统的用户都能对应到一个真实员工比如超级管理员。混在一张表里会让权限判断变得极不灵活。工资记录按月生成用唯一索引保证幂等。同一员工一个月只能有一条工资记录这个约束必须在数据库层面用组合唯一索引employee_idsalary_month兜底否则并发或重复提交时会产生脏数据。金额一律用DECIMAL(10,2)禁止用FLOAT或DOUBLE。浮点数在二进制里无法精确表示小数累加扣款时会出现0.10.2不等于0.3这种问题工资是钱钱不能错。DECIMAL(10,2)能精确到分十亿以内的金额都够用。2.2 核心表结构详解以下是我在这个系统里最常用的一套表设计主键统一用自增BIGINT时间字段用DATETIME逻辑删除用DELETED标记而非物理删除。部门表t_dept字段类型说明idbigint主键namevarchar(50)部门名称parent_idbigint父部门ID0表示根节点leader_idbigint负责人关联员工表create_timedatetime创建时间deletedtinyint逻辑删除标记parent_id支持多级部门树比如技术中心—研发一部。实际开发中要注意树递归查询的深度问题层级不深时在Service层用递归组装即可不必上递归SQL。员工表t_employee字段类型说明idbigint主键emp_novarchar(20)工号业务编号namevarchar(30)姓名dept_idbigint所属部门positionvarchar(50)岗位base_salarydecimal(10,2)基本工资entry_datedate入职日期statustinyint在职状态1在职 2离职emp_no工号在业务上具有唯一性应加唯一索引。base_salary放员工表还是工资表是个值得思考的问题——员工表存的是签约工资工资表存的是当月实际应发两者可能不一样比如某月绩效调整了基本工资所以我在员工表只存基础标准工资工资单里的实际项单独存。工资记录表t_salary_record字段类型说明idbigint主键employee_idbigint员工IDsalary_monthvarchar(7)薪资月份如2025-06basic_salarydecimal(10,2)基本工资performance_salarydecimal(10,2)绩效工资overtime_paydecimal(10,2)加班费allowancedecimal(10,2)补贴津贴deduct_absencedecimal(10,2)缺勤扣款deduct_socialdecimal(10,2)社保个人部分personal_taxdecimal(10,2)个人所得税gross_salarydecimal(10,2)应发合计net_salarydecimal(10,2)实发工资statustinyint状态0草稿 1已确认 2已发放create_timedatetime创建时间gross_salary和net_salary虽然可以从明细项推导出来但我仍然选择冗余存储。原因有两个第一统计SQL直接查这两个字段不用每次SUM一遍明细性能好第二工资单一旦确认发放明细项可能被后续调整影响冗余值保留的是当时的历史快照。计税逻辑建议放开计算函数把个税起征点、税率表作为系统参数存在配置表而不是硬编码。因为税法调整的频率比你想象中高得多硬编码意味着每次改税率都要发版。系统用户表t_sys_user字段类型说明idbigint主键usernamevarchar(50)登录名passwordvarchar(100)BCrypt加密密码employee_idbigint关联员工role_codevarchar(30)角色编码statustinyint启用状态last_login_timedatetime最后登录时间密码加密必须用BCrypt不要用MD5。MD5是摘要算法不是加密算法撞库成本极低。Spring Security自带BCryptPasswordEncoder即使没引入Spring Security也可以用spring-security-crypto包单独使用。2.3 表设计阶段的几个关键决策要不要建物理外键我的建议是逻辑外键——也就是在实体里保留dept_id、employee_id这些字段但不建FOREIGN KEY约束。原因是在实际业务中你经常要先删子后删父或者暂时保留脏数据外键约束会导致批量导入数据时频繁报错。可以在Service层保证引用完整应用层事务做完这事比数据库约束更灵活。当然如果你所在的团队要求强约束那也无可厚非只是后续做数据清洗会痛一点。工资月字段用varchar(7)还是date我用varchar(7)比如2025-06。因为工资月的概念没有日用DATE类型反而需要额外处理格式查询时还要做字符串截断。员工删除走逻辑删除工资记录不做删除只做作废。工资是财务数据删除是高风险操作更稳妥的做法是增加VOID状态把记录作废保留原始数据用于审计。这一点在后续做操作日志时也是同样的思路。3. 后端核心实现SpringBoot MyBatis3.1 项目分层与包结构后端代码我习惯按业务模块分包而不是按技术层分包。业务模块分包的意思是你一眼看过去就知道工资相关的代码都放在salary包里而不是所有Controller放一起、所有Service放一起。com.example.payroll ├── common // 通用类统一返回体、常量、异常处理 ├── config // 配置类CORS、拦截器、MyBatis插件 ├── security // 认证授权JWT工具、拦截器、注解 ├── module │ ├── auth // 登录认证模块 │ ├── employee // 员工管理模块 │ ├── dept // 部门管理模块 │ ├── salary // 工资管理模块 │ └── report // 统计报表模块每个模块内部再按controller / service / mapper / entity / dto / vo分包。这种结构的最大优势是新人接手时可以按模块快速定位代码而不是在几十个Controller文件里大海捞针。3.2 为什么用MyBatis的XML而不用注解标题写的是MyBatis所以Spring Data JPA那套我们不聊。但即便在MyBatis内部也有两种写法SQL注解写在Mapper接口上或者XML文件维护SQL。工资管理系统的真实开发中我强烈建议用XML方式。原因工资系统有大量动态SQL场景。比如工资列表查询条件可能是部门、月份、员工姓名、状态任意组合这种场景用where标签动态拼接SQL非常自然注解方式只能靠Provider类又难看又难维护。另外XML文件让SQL与Java代码物理分离可以直接用编辑器或数据库管理工具调试复杂SQL改完不需要重新编译。这在排查统计结果不对这类问题时效率高一个量级。一个典型的分页查询Mapper片段select idselectSalaryPage resultTypecom.example.payroll.module.salary.vo.SalaryRecordVO SELECT s.id, e.emp_no, e.name AS emp_name, d.name AS dept_name, s.salary_month, s.gross_salary, s.net_salary, s.status, s.create_time FROM t_salary_record s INNER JOIN t_employee e ON s.employee_id e.id INNER JOIN t_dept d ON e.dept_id d.id where if testsalaryMonth ! null and salaryMonth ! AND s.salary_month #{salaryMonth} /if if testdeptId ! null AND e.dept_id #{deptId} /if if testkeyword ! null and keyword ! AND (e.emp_no LIKE CONCAT(%, #{keyword}, %) OR e.name LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY s.salary_month DESC, e.emp_no ASC /select分页我用的是MyBatis的分页插件PageHelper配置一个Interceptor就能用前端传pageNum和pageSize返回PageInfo对象非常成熟。实体字段映射有个经典坑数据库列名是下划线风格emp_noJava属性是驼峰empNo在配置文件里开启map-underscore-to-camel-case: true后MyBatis会自动映射但如果SQL里用了别名比如e.name AS emp_name这种方法反而不会自动映射到empName解决方法是查询语句里就起驼峰别名或者明确指定resultMap。这里最容易出现明明数据库有值查出来全为null的问题。3.3 工资计算模块的设计工资计算是工资系统的业务核心但它最忌讳的就是把计算逻辑全部散落在Controller或Service里。我的习惯是单独抽一个SalaryCalculator组件Component public class SalaryCalculator { public SalaryCalcResult calculate(SalaryCalcContext context) { BigDecimal basic context.getBasicSalary(); BigDecimal performance context.getPerformanceSalary(); BigDecimal overtime context.getOvertimePay(); BigDecimal allowance context.getAllowance(); // 应发合计 基本 绩效 加班 补贴 BigDecimal gross basic.add(performance).add(overtime).add(allowance); // 缺勤扣款按比例扣除基本工资 BigDecimal absence context.getAbsenceDays() .multiply(basic) .divide(BigDecimal.valueOf(21.75), 2, RoundingMode.HALF_UP); // 社保个人部分由基础参数决定 BigDecimal social context.getSocialInsurance(); // 计税基数 应发 - 社保 - 起征点 BigDecimal taxable gross.subtract(social).subtract(BigDecimal.valueOf(5000)); if (taxable.compareTo(BigDecimal.ZERO) 0) { taxable BigDecimal.ZERO; } BigDecimal tax calculateTax(taxable); // 实发 应发 - 扣款 - 社保 - 税 BigDecimal net gross.subtract(absence).subtract(social).subtract(tax); return SalaryCalcResult.builder() .grossSalary(gross) .absenceDeduct(absence) .socialInsurance(social) .personalTax(tax) .netSalary(net) .build(); } private BigDecimal calculateTax(BigDecimal taxable) { // 按月税率表计算个税税率和速算扣除数建议从配置表读取 if (taxable.compareTo(BigDecimal.valueOf(3000)) 0) { return taxable.multiply(BigDecimal.valueOf(0.03)); } if (taxable.compareTo(BigDecimal.valueOf(12000)) 0) { return taxable.multiply(BigDecimal.valueOf(0.10)) .subtract(BigDecimal.valueOf(210)); } // 更多区间省略实际应抽取为配置 return taxable.multiply(BigDecimal.valueOf(0.20)) .subtract(BigDecimal.valueOf(1410)); } }这段代码里最值得注意的细节是BigDecimal的运算方式。divid(float/double)是浮点运算精度不可控必须用divide(BigDecimal, scale, roundingMode)指定小数点后位数与舍入模式。工资场景一律用RoundingMode.HALF_UP也就是四舍五入。税率表我强烈建议做成一张t_tax_config配置表存级距上限、税率、速算扣除数。这样每年税法调整时只需在管理后台改配置不用改Java代码重新部署。这也是我在真实项目里被坑过之后才学乖的——原来硬编码的税率表随着税改直接算错了一个月工资财务部门追着我问了一周。3.4 权限设计JWT 拦截器工资信息天然敏感所以权限不能只是前端隐藏按钮后端每个接口都要校验。我的做法是登录接口校验用户名密码成功后签发JWTJWT里放userId和roleCode。用一个拦截器统一处理所有/api/**请求从请求头Authorization里取Token并解析。需要精确控制的接口上加自定义注解RequireRole(admin)拦截器里做角色比对。自定义注解的设计很简单Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); }拦截器里的核心逻辑大概是Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod handlerMethod)) { return true; // 放行非Controller请求 } RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole null) { return true; // 无需特殊角色 } Long userId (Long) request.getAttribute(userId); String roleCode userService.getRoleCode(userId); for (String allowedRole : requireRole.value()) { if (allowedRole.equals(roleCode)) { return true; } } // 权限不足返回401或403 response.setStatus(403); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:403,\msg\:\无权限访问\}); return false; }注意我没有直接在拦截器里解析JWT后再去数据库查角色而是选择在解析Token后从Redis或数据库查一次角色。原因是角色可能在中途被修改直接信任Token里的角色有权限延迟问题虽然多一次查询但换来的是权限即时生效。对工资系统这种敏感场景我可以接受这一点点性能开销。登录密码校验用BCrypt接口返回时永远不要把密码字段序列化出去。一个简单做法是在SysUser实体上加JsonIgnore或者返回VO对象时只放必要字段我在真实项目里两种方式都用过更推荐后者——返回VO对象是一个好习惯避免无意中把实体细节暴露给前端。3.5 导入导出工资系统的隐藏痛点每个月发薪前HR手里大概率有一个Excel工资表系统如果能直接导入工作量大减。导出则是因为财务系统或其他部门需要留档。这里直接用阿里开源的EasyExcel比Apache POI好用太多内存占用低API也简单。导出工资单的核心代码GetMapping(/export) public void export(String salaryMonth, HttpServletResponse response) throws IOException { ListSalaryRecordVO list salaryService.listByMonth(salaryMonth); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); String fileName URLEncoder.encode(工资明细- salaryMonth, UTF-8); response.setHeader(Content-disposition, attachment;filename fileName .xlsx); EasyExcel.write(response.getOutputStream(), SalaryRecordVO.class) .sheet(工资单) .doWrite(list); }Excel的ExcelProperty注解用起来也很直接ExcelProperty(工号) private String empNo; ExcelProperty(姓名) private String empName; ExcelProperty(value 应发工资) private BigDecimal grossSalary;导入时最大的坑是模板校验。EasyExcel提供了ReadListener接口你可以逐行校验数据但建议导入前先下载标准模板服务端再做一次必填项和数据类型校验否则遇到格式错误的Excel直接抛异常用户看到的就是一长串英文报错体验极差。我做导入时会把错误行号和数据问题汇总到一个Excel里返回给用户让用户知道第3行工号不合法第5行工资不能为空这样反思效率高。导出时另一个常见的段错误是金额字段导出后变成了浮点数比如5000.00变5000.0。解决方式是在ContentStyle或EasyExcel的Converter里指定小数位数格式或者在VO字段上标注NumberFormat(0.00) private BigDecimal netSalary;4. 前端Vue3实现从零到可用的后台界面4.1 项目初始化与技术选型前端用Vite创建工程命令是npm create vitelatest payroll-web -- --template vue-ts。工程化层面我加了以下依赖vue-router4前端路由与路由守卫pinia全局状态管理主要存用户信息和权限状态element-plus后台管理系统的UI利器表格、表单、弹窗、分页全都现成axiosHTTP请求库封装请求和响应拦截器sass样式预处理全局变量管理没有引入太重的东西TSTypeScript看个人偏好如果你还不熟TS可以先用纯JavaScriptVue3对JS的兼容性很好。但从长期维护角度我还是建议上TS组件Props、接口返回值的类型定义能直接避免一半的undefined错误。4.2 前端页面结构整个前端界面按后台管理系统的经典布局组织├── src │ ├── api // 按模块拆分的请求函数 │ ├── router // 路由配置 │ ├── stores // Pinia状态 │ ├── layouts // 主布局组件侧边栏顶栏内容区 │ ├── views │ │ ├── login // 登录页 │ │ ├── dashboard // 首页仪表盘 │ │ ├── employee // 员工管理页 │ │ ├── salary // 工资管理页 │ │ ├── report // 统计报表页 │ │ └── system // 用户管理与操作日志 │ └── utils // axios封装、格式化工具页面组件的设计要点是列表页搜索表单表格分页弹窗或抽屉。Element Plus的el-table、el-pagination、el-dialog基本能覆盖80%的后台界面需求。不要过度抽象组件像查询条件区这种完全可以用一个方法复用强行封装反而增加心智负担。4.3 Axios拦截器与Token管理前端与后端交互的核心是Token。登录接口返回JWT后我把它存到Pinia和localStorage里。Axios请求拦截器在每次请求前把Token加到请求头service.interceptors.request.use( (config) { const token useUserStore().token; if (token) { config.headers.Authorization Bearer ${token}; } return config; }, (error) Promise.reject(error) );响应拦截器里最关键的逻辑是统一处理401和业务错误码service.interceptors.response.use( (response) { const res response.data; if (res.code ! 200) { ElMessage.error(res.msg || 请求失败); return Promise.reject(new Error(res.msg || Error)); } return res.data; }, (error) { if (error.response?.status 401) { useUserStore().logout(); router.push(/login); } else { ElMessage.error(error.response?.data?.msg || 服务器开小差了); } return Promise.reject(error); } );我踩过的坑不要直接拿后端异常信息弹给用户比如SQLIntegrityConstraintViolationException这种内容用户看了只会一头问号。后端应该统一返回{code: 500, msg: 员工工号已存在}这类人话前端只管弹msg。4.4 路由守卫与角色权限前端路由守卫的作用是拦截未登录用户和控制菜单可见性它只是体验优化真正的安全防线在后端。但界面上该做的还是要做router.beforeEach((to, from, next) { const userStore useUserStore(); if (to.path /login) { next(); return; } if (!userStore.token) { next(/login); return; } if (to.meta.roles !to.meta.roles.includes(userStore.roleCode)) { next(/403); return; } next(); });动态菜单的实现方式是在路由表里给每个页面标记meta.roles比如工资管理页meta: { roles: [admin, finance] }侧边栏组件根据当前用户角色过滤掉没有权限的菜单项。这样普通员工登录后看不到员工管理系统设置这类入口只会看到我的工资条。4.5 表单与表格的联动细节工资管理页是前端最复杂的地方。一个发薪月的操作流程大概是选择月份点击生成工资单按钮后端根据员工表基础工资当月绩效参数批量生成草稿工资。表格展示所有员工工资财务可逐行修改绩效、加班费、扣款。点击确认后锁定记录禁止修改。点击导出下载Excel。前端实现上要注意编辑弹窗和表格数据同步的问题。我在保存单条工资记录时调完后端接口后不会重新拉取整个列表而是用返回值更新本地表格对应行的数据。但如果修改了多行还是建议重新调用分页查询接口保证列表和数据库一致。Vue3里一个常见的坑是直接修改数组某一项的属性不触发视图更新。比如tableData.value[idx].netSalary 5000。如果你用了reactive数组再直接按索引改属性视图不一定刷新。这里推荐用ref保存表格数据配合Vue3的自动响应机制或者修改后整体重新赋值tableData.value [...newData]。4.6 Vue3开发里的几个实操心得Element Plus的日期组件返回的是Date对象转成yyyy-MM-dd时要自己封装格式化函数不要直接传给后端容易有时间和时区误差。表格列宽设置min-width而不是固定width否则在小屏上会横向溢出。弹窗组件用v-model控制显隐时注意destroy-on-close属性否则上一次打开的临时输入会残留在弹窗里。前端表单调接口前的必填校验尽量用Element Plus自带规则简单有效但业务逻辑校验比如实发工资不能为负还得在前端做一次这是给用户即时反馈后端也要做一次这是兜底安全。我用这套方案做过好几个类似项目前端部分最耗时间的往往是无休止的对齐细节所以开发时建议直接用el-card el-form el-table这套组合把精力放在业务逻辑上而不是造轮子写一个自己的UI框架。5. 全流程跑通与部署实战5.1 本地环境准备开始复现之前先把环境准备好版本不匹配是第一个大坑JDK17SpringBoot 3.x要求JDK17及以上如果用的SpringBoot 2.7则JDK8也够MySQL8.0及以上注意用mysql-connector-j驱动版本要和MySQL服务端兼容Node.js18以上Vite 5要求这个版本Maven3.6以上最好用IDEA自带的Maven前端包管理器用npm或pnpm都行推荐pnpm安装速度更快5.2 后端启动步骤用IDEA导入项目等待Maven下载依赖。如果网络不好在settings.xml里配阿里云镜像仓库。修改application.yml里的数据库连接信息server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/payroll_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true在数据库执行项目自带的init.sql脚本如果源码里有的话没有就按上面章节的表结构自己建。脚本最好包含初始化管理员账号。直接运行启动类看到Tomcat started on port 8080即成功启动。首次启动如果报驱动类找不到检查依赖里是否引入了对应版本的mysql-connector-j。5.3 前端启动步骤npm install或pnpm install安装依赖这一步常遇到的问题node-sass安装失败、某依赖版本冲突。我一般直接删掉node_modules和package-lock.json重新安装大多数情况都能解决。配置Vite代理解决跨域在vite.config.ts里export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } });这样前端请求/api/salary/list自动代理到后端http://localhost:8080/salary/list本地开发完全不需要后端配CORS。npm run dev启动浏览器访问http://localhost:3000。5.4 生产部署步骤本地跑通后部署到云服务器或者公司内网服务器两个部分分开打包后端打包mvn clean package -DskipTests java -jar target/payroll-server.jar --spring.profiles.activeprod前端打包npm run build # 生成 dist 目录用Nginx托管Nginx配置的核心点server { listen 80; server_name your-domain.com; location / { root /opt/payroll-web/dist; index index.html; try_files $uri $uri/ /index.html; # 前端路由history模式必须加 } location /api/ { proxy_pass http://127.0.0.1:8080/; # 注意末尾斜杠去掉/api前缀 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files这行是Vue Router的history路由模式必须的否则刷新页面会404。如果不想处理这个也可以把前端路由模式换成hash但URL里会多个#观感略差。5.5 服务器部署的几个坑MySQL时区问题服务器上MySQL默认时区可能是UTC插入时间会比本地少8小时。在JDBC连接串里加serverTimezoneAsia/Shanghai并在MySQL里执行set global time_zone 8:00;。端口被占用netstat -tlnp | grep 8080查看是否被别的进程占着改端口或杀进程。服务器内存不足云服务器如果只有1G内存Java项目加-Xmx512m限制堆内存否则容易OOM。数据库连接池连接数小项目默认的HikariCP够用但并发导Excel时可能占用较多连接适当调大maximum-pool-size到20。6. 常见问题与排查技巧实录6.1 前后端联调时的经典问题现象可能原因解决方案前端请求404代理没配好或后端接口路径不对先看后端日志是否有请求进入检查Vite代理rewrite规则前端请求正常但无数据后端返回了但字段名对不上或查询条件错误打开浏览器Network看返回JSON和后端VO字段对比登录后刷新页面又跳到登录页Token没存localStorage或路由守卫从store取token失败检查Pinia初始化时是否从localStorage读取token表格日期显示不对后端返回时间戳前端没有格式化处理后端序列化格式或在前端用dayjs统一格式化导出Excel出现乱码文件名或内容编码问题后端设置Content-type与Content-Disposition时要带UTF-8批量导入时Excel报错模板列头和实体字段对不上下载标准模板检查ExcelProperty的value是否一致6.2 后端常见的运行异常MyBatis报Invalid bound statement (not found)错误八成是Mapper接口的XML文件没有被扫描到。检查mapper-locations配置是否正确XML文件是否在target/classes目录里Mapper接口是否加了Mapper或启动类是否用了MapperScan我遇到过一次因为Maven编译时把XML排除了的问题后来在pom里加resources resource directorysrc/main/resources/directory includes include**/*.xml/include /includes /resource /resources查询出的部分字段为null大概率是下划线转驼峰没生效。确认map-underscore-to-camel-case: true已配置并且SQL别名的写法与Java属性名严格对应。比如Java属性是netSalarySQL别名写net_salary反而映射不了应该写netSalary或者用resultMap显式映射。启动时提示Failed to configure a DataSourceSpringBoot自动配置要连数据库但你还没配置数据源。检查application.yml里是否少了spring.datasource配置或者多引入了某个依赖导致自动配置冲突。6.3 排错思路实录从工资算错了到定位这个场景我可以展开讲讲。某天财务反应6月实发工资普遍少了20多块查看数据库发现net_salary值不对。第一反应不是改代码而是先看计算日志——我在SalaryCalculator里每个关键步骤都打了日志应发、缺勤扣款、社保、个税、实发打开日志发现是calculateTax方法里某一档税率计算结果偏大。再对比税率表发现是配置表里某个级距上限写错了。因为我把税率表外置到数据库后录入人员不小心把12000写成了1200导致大量拿8000块钱工资的员工被多算了税。修复配置表后重跑工资计算脚本全部纠正。这个案例说明两件事计算组件要打详细日志税率配置要有二次确认机制。别嫌日志多出了问题它就是救命稻草。6.4 几个值得坚持的工程习惯Controller只做参数接收与响应业务逻辑全部下沉到Service。一个Controller里不要超过5行业务代码否则后期会变成无人敢改的屎山。所有列表查询分页返回统一格式。我习惯用PageResultT结构total、list、pageNum、pageSize前端拿到直接填充表格不用每次做适配。接口对敏感操作记录操作日志。比如导出工资单确认工资单这类操作用户ID、操作时间、操作内容、IP地址都记录到t_operation_log出了事能追溯是谁干的。不要在生产环境开SQL日志打印。MyBatis的日志会打印完整SQL和参数工资数据可不想凭空泄露给所有日志查看者。生产环境把日志级别调到WARN。7. 从能用到好用这套系统能扩展什么工资管理系统做完能跑通只能说完成了一半。真实业务里它一定会被提出各种新需求下面几个方向是我觉得最值得继续做的绩效联动目前工资单里的绩效工资是手工填的可以对接绩效考核模块每月自动根据考核分数计算绩效工资减少财务手工操作也减少人为错误。审批流工资单确认之前加一个提交→审核→通过流程财务录完工资后由负责人审核审核通过后才能发放。可以用Flowable或Camunda这类工作流引擎也可以简单用状态机自己实现。薪资分析看板前端接入ECharts统计各部门平均工资、工资成本占比、年度薪资趋势。这些图表数据后端用GROUP BY和聚合SQL就能提供难度不大但给管理层的直观感受比一堆表格强得多。多租户支持如果你所在的公司是做人力外包的系统那这套工资管理系统可能需要支持多家企业独立使用也就是在核心表上增加tenant_id字段并调整所有SQL的过滤条件。这是从单组织系统到SaaS平台的关键一步。离线工资条很多员工不愿意登录系统查看工资条可以做一个PDF版本的工资条下载或者生成加密链接发到员工邮箱。这个功能用POI或iText就能实现但要注意PDF的生成速度几百人的公司一次生成没问题几千人就要考虑异步队列了。我个人在实际做这套系统时最深的体会是工资管理系统看起来简单但它的复杂性全部藏在边界和细节里。金额精度、权限隔离、税率配置、异常数据兜底每一条在演示时都不会暴露一旦上生产很快就变成事故。所以如果你准备复现或改造这个项目我强烈建议你在做数据库表设计时多花一倍时间把它当成一个真正的财务系统对待而不是一个练习CRUD的玩具。等这套流程跑顺了你再去面对其他业务系统会发现很多思想是通用的——数据模型稳系统才能稳。