ARTICLE DETAIL

资讯详情

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

基于SpringBoot+Vue3+MyBatis+MySQL的养老保险管理系统实战解析

基于SpringBoot+Vue3+MyBatis+MySQL的养老保险管理系统实战解析 1. 为什么是 SpringBoot Vue3 MyBatis MySQL 这套组合先说个结论养老保险管理系统这种业务技术栈选型从来不是越新越好而是越稳越好。这套系统我用 SpringBoot 做后端、Vue3 做前端、MyBatis 做数据持久层、MySQL 存数据前后端完全分离整个项目跑下来非常顺特别适合做毕设、面试项目或者给中小型单位做内部管理系统参考。你可能会问市面上那么多框架为什么偏偏是这四样我拆开说。SpringBoot 解决的是后端工程怎么搭的问题。它对 Spring 那一大堆 XML 配置做了自动装配内嵌 Tomcat打成一个 jar 包就能跑。对养老保险这种 CRUD 密集型的业务系统来说SpringBoot 提供的 Starter 机制能帮你省掉大量重复配置比如数据源、事务、Jackson 序列化这些基本都是引入依赖就生效。而且国内 Java 招聘市场对 SpringBoot 的接受度极高项目写这个技术栈无论找工作还是做毕设答辩都站得住脚。Vue3 解决的是前端交互怎么做的问题。为什么不用 Vue2Vue3 的 Composition API 在逻辑复用上的优势太明显了——养老保险系统里有参保登记、缴费管理、待遇发放、账户查询这些功能页面页面之间有很多公共逻辑比如分页、表单校验、字典数据回显用 Composition API 可以很干净地抽出 useTable、useForm 这类组合式函数。而且 Vue3 搭配 Vite 开发冷启动速度和热更新体验比 Vue2 Webpack 强太多了开发效率完全不在一个量级。你要是用过 Vue2 里那种 mixins 到处混的写法再切到 Vue3 的 setup 语法基本就回不去了。MyBatis 解决的是SQL 怎么管理的问题。MyBatis 特有的动态 SQL 在处理养老金的复杂条件查询时太顺手了——比如参保人查询可能要组合姓名、身份证号、参保状态、参保时间区间好几个条件用 if 标签可以非常灵活地拼接 SQL 片段。而且相比 JPAMyBatis 对 SQL 的可控性更强DBA 审起 SQL 来也方便像养老金这类对数据准确性要求极高的系统SQL 写在自己的掌控范围内更让人放心。MyBatis 缓存机制一级缓存、二级缓存也值得单独拿出来说后面我会详细展开。MySQL 解决的是数据往哪存的问题。轻量、免费、社区活跃8.0 版本之后窗口函数、CTE公共表表达式这些能力也跟上来了对养老系统的各种统计报表比如按月/按险种统计参保人数很有帮助。单一 MySQL 实例对于绝大多数内部管理系统来说性能绰绰有余不需要一上来就上分布式数据库。注意这套系统里我说的前后端分离是指前端工程和后端工程完全独立——前端用 Vite 起开发服务比如 5173 端口后端独立跑在 8080 端口两者通过 HTTP 接口 JSON 数据交互前端代码、后端代码分目录存放可以分别部署到 Nginx 和服务器上的独立进程。这套组合的定位非常清晰给凡是表单 列表 权限 报表类型的业务系统提供一个可以直接抄作业的样板工程。它不像微服务那么重但足够让你理解企业级项目的组织方式统一的返回结果封装、全局异常处理、JWT 登录鉴权、前端路由守卫、Pinia 状态管理……这些不是一个 Demo而是一套可扩展的骨架。2. 养老保险系统的业务拆解参保、缴费、发放、账户四条主线聊技术之前得先把这个系统的业务底盘说清楚。养老保险管理系统表面看是人员信息增删改查但真正落地的时候你会发现业务逻辑远不止这么简单。2.1 核心业务模块清单一个完整的养老保险管理系统至少要覆盖下面这些模块参保管理单位参保登记、个人参保登记、参保状态变更参保/停保/断缴/恢复、参保信息修改。这块是所有业务的数据源头身份证号是唯一业务主键。缴费管理按月/按年度生成缴费计划、记录实际缴费流水、单位缴费与个人缴费分账处理、欠费与补缴管理。待遇发放退休人员养老金计算、按月生成待遇发放名册、发放记录留痕。个人账户管理个人账户建立、缴费记录计入个人账户、账户利息结转按当地政策年利率计息。系统管理用户管理、角色管理、菜单权限管理、操作日志、数据字典。这些模块之间的逻辑关系很简单先有参保才能缴费缴费记录进个人账户也作为待遇计算的基数满足退休条件后进入待遇发放环节。2.2 数据库表设计的关键决策基于上面的业务拆解我设计的核心表结构如下只列关键字段表名关键字段作用说明personid, id_card, name, gender, birth_date, phone, address, status参保人基础信息表id_card 做唯一索引unitid, unit_name, credit_code, contact, phone, address参保单位表person_unitid, person_id, unit_id, start_date, end_date, status参保人与单位的关联关系表处理一个人在多个单位参保过的情况insurance_recordid, person_id, type, related_unit_id, insurance_date, status参保登记记录表type 区分首次参保/恢复参保payment_planid, person_id, plan_month, base_amount, personal_ratio, unit_ratio, due_amount缴费计划表按人按月生成payment_recordid, person_id, plan_id, paid_month, pay_amount, pay_date, pay_channel, status实际缴费流水表account_detailid, person_id, change_type, amount, balance_after, change_date, remark个人账户流水表每次金额变动写入一行pension_recordid, person_id, benefit_year, benefit_month, amount, status, issue_date待遇发放记录表sys_userid, username, password, real_name, status系统用户表登录账号sys_roleid, role_name, role_code, remark角色表sys_user_roleuser_id, role_id用户-角色关联sys_menuid, parent_id, menu_name, path, component, icon, sort菜单表sys_role_menurole_id, menu_id角色-菜单权限关联其中有两张表的设计需要特别展开说。第一张是person_unit。很多没做过社保业务的人会把单位 ID直接塞进 person 表但这在真实业务里是行不通的——一个人可能先在 A 单位参保后来换到 B 单位中间还可能自己以灵活就业身份参保。如果把单位 ID 直接挂在 person 表上历史记录就全丢了。所以单位关联关系必须单独成表记录时间区间查询某人某年某月在哪个单位参保就非常方便。第二张是account_detail。养老保险的个人账户本质上是一个只增不减特殊情况除外的资金账户。把每次计入、结息、调整都写成一条流水余额实时计算这是最稳妥的设计模式。好处显而易见对账方便出问题能追溯月底对账直接 SUM 流水就能算出总余额不用维护一个可能被改坏余额字段。2.3 权限模型怎么设计这个系统的权限我用的是最经典的 RBAC基于角色的访问控制模型用户-角色-菜单三层。用户拥有若干个角色角色关联若干个菜单菜单对应前端路由和后端接口权限标识。实现时的关键点是后端接口必须二次校验权限不能只靠前端隐藏按钮。前端路由守卫控制的是页面能不能进但接口层面的权限校验需要后端配合。我的做法是在 SpringBoot 中定义一个RequiresPermission(system:user:add)这样的自定义注解加在 Controller 方法上拦截器里解析当前用户角色拥有的权限标识列表做匹配校验。这样即使有人直接拿着 token 去调接口也过不了这一关。3. 后端落地的关键一环SpringBoot 配置与 MyBatis 的实战配合3.1 SpringBoot 工程结构怎么摆我个人习惯把后端工程按功能分包而不是按技术类型分包com.example.pension ├── common │ ├── result // 统一的返回结果封装 ResultT │ ├── exception // 自定义异常 全局异常处理器 │ ├── constant // 常量定义 │ └── utils // 工具类JWT工具、日期工具等 ├── config // 配置类CORS配置、MyBatis配置、拦截器注册等 ├── controller // 控制器层 ├── service │ ├── impl ├── mapper // MyBatis Mapper接口 ├── entity // 实体类 ├── dto // 请求/响应数据传输对象 └── vo // 视图对象给前端返回的DTO这样分包的好处是从 controller 进来每一层职责非常清晰找代码快。如果按技术类型分包entity 全放一起、mapper 全放一起一个业务模块跨好几个包改一个功能跳来跳去很浪费时间。3.2 SpringBoot 核心配置的坑与调优我先贴一份我的application.yml中比较关键的部分spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pension_system? useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/Shanghai useSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.pension.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有几个坑逐个说。数据库连接 URL 里的参数绝对不能省。serverTimezoneAsia/Shanghai必须加不加的话 MySQL 8.x 驱动会报 CST 时区的错尽管报错信息让人看不懂实际就是驱动拿不到正确的时区。allowPublicKeyRetrievaltrue是 MySQL 8.0 用 caching_sha2_password 加密插件时需要的参数不加的话连数据库会报Public Key Retrieval is not allowed。map-underscore-to-camel-case必须打开。数据库字段一般是id_card、real_name这种下划线命名Java 实体是idCard、realName驼峰命名。不开启这个配置的话你得给每个字段写 resultMap 映射非常繁琐。打开之后MyBatis 自动帮你把下划线转驼峰省一大半 XML 代码。log-impl: StdOutImpl是开发环境排错利器。它会让 MyBatis 在控制台把每条 SQL 及参数直接打印出来。你可以在开发阶段打开上线前关掉或者换成 logback 输出到文件。热搜词里提到的 idea mybatis log free 这个插件也是干这个的能从 MyBatis 日志里把带参数的完整 SQL 还原出来复制到 Navicat 里直接执行排查效果比看控制台原生日志更直观。3.3 MyBatis 动态 SQL复杂养老业务查询的救星养老金业务里最常见的查询场景是综合条件查询比如在参保人管理页面用户可能同时按姓名、身份证号、参保状态、所属单位、参保时间范围来筛选。每个条件都可能为空全为空则查全部。这种需求MyBatis 的whereif是标准解法select idselectPersonList resultTypecom.example.pension.vo.PersonVO SELECT p.id, p.id_card, p.name, p.birth_date, p.phone, p.status, u.unit_name FROM person p LEFT JOIN person_unit pu ON p.id pu.person_id AND pu.status ACTIVE LEFT JOIN unit u ON pu.unit_id u.id where if testname ! null and name ! AND p.name LIKE CONCAT(%, #{name}, %) /if if testidCard ! null and idCard ! AND p.id_card #{idCard} /if if teststatus ! null and status ! AND p.status #{status} /if if testunitId ! null AND u.id #{unitId} /if if teststartDate ! null AND p.insurance_start_date gt; #{startDate} /if if testendDate ! null AND p.insurance_start_date lt; #{endDate} /if /where ORDER BY p.create_time DESC LIMIT #{offset}, #{pageSize} /selectwhere标签会自动处理掉第一个条件前面的 AND不用写WHERE 11这种很丑的写法。gt;和lt;是因为 XML 中和需要转义实际执行的 SQL 是和。这点如果忘了XML 解析直接报错新手经常在这里卡半天。3.4 MyBatis 缓存机制用对了提效用错了出大事MyBatis 的缓存是面试题里的常客也是实际项目中容易被忽略的点。一级缓存是 SqlSession 级别的缓存默认开启同一个 SqlSession 内执行相同的 SQL 会直接命中缓存。但要注意Spring 管理的 Service 方法中每次执行 Mapper 方法可能都会新开或复用不同的 SqlSession所以在 SpringBoot MyBatis 的实际项目中一级缓存的作用很有限。二级缓存是 Mapper 级别的缓存多个 SqlSession 共享需要手动开启。但在我做养老系统的时候明确不开启二级缓存。原因有三养老保险系统对数据准确性要求极高二级缓存的失效机制在关联查询多的情况下容易出问题一个表的更新不会自动清掉另一个表相关的缓存项数据变动频繁脏读风险大都走 Redis 做缓存了如果真要缓存的话MyBatis 二级缓存显得鸡肋。如果你要做面试项目展示可以在一个查询频率高且数据基本不变的数据字典表上开启二级缓存作为亮点讲。但业务数据表我的建议始终是别开。3.5 SpringBoot MyBatis 当表不存在自动建表这个是我在实际部署时遇到的一个需求——新环境部署时希望系统首次启动能自动把表结构建好省得手动导入 SQL 文件。网上讨论 SpringBoot MyBatis 做动态建表的方案不少我用的是一种稳妥的折中方案项目启动时执行一个初始化 SQL 脚本文件。在application.yml中配置 Spring 的 SQL 初始化spring: sql: init: mode: always schema-locations: classpath:db/schema.sql encoding: utf-8注意schema.sql里的建表语句都要写成CREATE TABLE IF NOT EXISTS这样已有的表不会重复创建不存在的表才建起到典型的幂等效果。数据初始化脚本可以用>import axios from axios import { ElMessage } from element-plus import { useUserStore } from /store/user import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带 token request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) // 响应拦截器统一处理错误码和 HTTP 401 request.interceptors.response.use( response { const res response.data // 后端统一返回 { code: 200, msg: success, data: ... } if (res.code 200) { return res.data } ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) }, error { if (error.response?.status 401) { const userStore useUserStore() userStore.clearToken() router.push(/login) ElMessage.warning(登录状态已过期请重新登录) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default request这个封装有几个关键点统一拿后端返回的code字段判断业务成功与否HTTP 401 统一跳登录页并清 token前端不关心中间层具体路径开发环境用 Vite 代理转发到后端。配套地在vite.config.js中配置代理server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端代码里请求地址写/api/person/list就够了不用写死后端地址换环境部署只需要改代理配置。4.2 Pinia 与 Vuex 对比为什么选 PiniaVue3 官方推荐的状态管理库已经是 Pinia 了。Vuex 为 Vue3 写的 4.x 版本能用但 Pinia 天然为 Composition API 设计类型推断更好没有 mutations 那一层冗余设计直接在 store 里定义 actions 改 state代码量少一半。在养老系统里我开了三个 storeuserStore登录用户信息、token、权限标识列表appStore侧边栏折叠状态、全局加载状态dictStore各类数据字典参保状态、缴费类型、险种类型等。其中dictStore很有讲究。字典数据是系统里的高频冗余数据——前端下拉框要展示参保状态数据库里存的是ACTIVE、STOPPED这种编码具体显示成参保中、已停保是字典表的事。我做一个dictStore登录后一次性拉取所有字典到内存页面上直接dictStore.getLabel(personStatus, row.status)就能拿到展示文字不需要每个页面单独请求。这比每个下拉框都发一次接口查询要高效得多。4.3 路由守卫登录校验 权限控制 动态注册路由守卫是前后端分离项目的大门保安。我的路由设计分两层静态路由login、404、403 这几个页面所有人可访问。动态路由系统菜单对应的业务页面登录后根据用户角色从后端获取可访问的路由表用router.addRoute()动态注册。核心伪代码如下router.beforeEach(async (to, from, next) { const userStore useUserStore() if (!userStore.token) { // 未登录只能进 login if (to.path /login) { next() } else { next(/login?redirect to.fullPath) } } else { // 已登录但还没拿到用户信息和菜单 if (!userStore.userInfo) { try { await userStore.fetchUserInfo() // 获取基本信息 const menuRoutes await userStore.fetchMenus() // 获取动态菜单 menuRoutes.forEach(route router.addRoute(route)) // 防止刷新页之后 addRoute 还没完成导致 404 next({ ...to, replace: true }) } catch (e) { userStore.clearToken() next(/login) } } else { next() } } })这里有一个必须处理的 bug刷新页面时Pinia 里的动态路由数据会丢失得重新拉取菜单并 addRoute。如果不做next({ ...to, replace: true })这一步用户刷新之后大概率会被 404 页面拦住。4.4 组合式函数抽公共逻辑useTable 的实战写法Vue3 相比 Vue2 最大的优势之一就是可以通过组合式函数抽离公共逻辑。做后台管理系统列表页面的套路出奇一致搜索表单 表格 分页。以前每个页面都写一份分页、加载、搜索逻辑用了useTable之后就清爽多了// composables/useTable.js import { ref, reactive } from vue export function useTable(fetchData, options {}) { const loading ref(false) const dataList ref([]) const total ref(0) const queryParams reactive(options.queryParams || {}) const pagination reactive({ currentPage: 1, pageSize: 10 }) async function loadData() { loading.value true try { const params { pageNum: pagination.currentPage, pageSize: pagination.pageSize, ...queryParams } const res await fetchData(params) dataList.value res.records total.value res.total } finally { loading.value false } } function handleSearch() { pagination.currentPage 1 loadData() } function handleReset() { Object.keys(queryParams).forEach(key { queryParams[key] undefined }) handleSearch() } function handlePageChange(page, pageSize) { pagination.currentPage page pagination.pageSize pageSize loadData() } return { loading, dataList, total, pagination, queryParams, loadData, handleSearch, handleReset, handlePageChange } }页面上只要这样用const { loading, dataList, total, pagination, queryParams, handleSearch, handleReset, handlePageChange } useTable(personApi.getPageList)省掉的重复代码量十分可观。这也是面试时和组织者聊 Vue3 的最佳素材之一——你用组合式函数抽了什么公共逻辑比你会用 v-model要加分得多。5. MySQL 使用细节与养老系统的数据安全设计5.1 MySQL 8.0 安装配置里的高频问题MySQL 这块网上的安装教程一搜一大堆但实际装的时候还是有几个坑反复出现。我先说一个最重要的8.0 默认的认证插件是 caching_sha2_password而很多旧版客户端和部分连接池版本不认识它。如果你用 5.x 的 JDBC 驱动连 8.0 的库或者拿很老的 Navicat 连大概率会报Authentication plugin caching_sha2_password cannot be loaded。解决办法有两个要么升级驱动/客户端到支持 8.0 的版本要么在 MySQL 里把用户认证插件改回 mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;我的建议是以升级客户端为主因为 mysql_native_password 在新版本 MySQL 中已被标记为弃用将来某天可能会移除。改认证插件只能是应急方案。另一个常见的坑是 MySQL 8.0 安装完后 root 账号的密码规则。如果你设了一个简单密码比如123456MySQL 可能会拒绝创建因为它默认要求中强度以上的密码策略validate_password 组件。这种情况可以临时把密码策略调低SET GLOBAL validate_password.policy LOW;具体参数名在不同版本里略有差异有的叫validate_password_policy你执行的时候可以先用SHOW VARIABLES LIKE validate_password%;看一下实际名字。5.2 存储过程在养老金计算里的用武之地热搜词里提到了 mysql 存储过程养老系统里正好有一个典型的适用场景——按人员缴费月数、账户余额、当地上年度社平工资等参数批量计算养老金。这类计算的特点是涉及多张表数据的读取、多步计算、结果写入另一张表而且一旦参数确定比如退休审批完成过程要保证整体执行、不能说算到一半停了。用存储过程可以把这一串逻辑包成一个数据库操作-- 简化示例批量生成某月养老金发放记录 CREATE PROCEDURE generate_pension_record(IN calc_month VARCHAR(7)) BEGIN DECLARE done INT DEFAULT FALSE; DECLARE p_person_id BIGINT; DECLARE cur CURSOR FOR SELECT id FROM person WHERE status RETIRED AND id NOT IN ( SELECT person_id FROM pension_record WHERE benefit_month calc_month ); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO p_person_id; IF done THEN LEAVE read_loop; END IF; -- 根据个人账户余额、缴费年限等计算金额 INSERT INTO pension_record(person_id, benefit_year, benefit_month, amount, status, issue_date) SELECT p_person_id, LEFT(calc_month, 4), calc_month, -- 具体的计算逻辑这里简化了 (SELECT ...), WAIT_CONFIRM, NOW(); END LOOP; CLOSE cur; END;当然业务计算放存储过程会带来一定维护成本版本管理不方便、难以调试所以我只在纯 SQL 能高效完成且事务性强的场景用存储过程复杂的业务计算还是放 Java Service 层做。这个平衡点你也要自己把握。5.3 这系统里的数据安全机制养老金的钱不是小数目系统在数据安全上不能裸奔。我落地了几条硬措施密码不能明文存储系统用户表的密码用 BCrypt 加密Spring Security 自带 BCryptPasswordEncoder同一个密码每次加密结果都不一样数据库泄露也没法反推。千万别用 MD5MD5 加盐也尽量别用了BCrypt 是更稳的选择。关键操作留审计日志待遇金额修改、用户权限变更、数据导出这几种高风险操作必须记录操作人、操作时间、变更详情。出了问题能追责到具体人。数据库权限分级管理应用连接数据库的账号只授 SELECT/INSERT/UPDATE/DELETE 权限不授 DDL 权限建表/删表那种这样即使应用被拖库攻击者也没法拉系统表改数据。金额字段用 DECIMAL不用 DOUBLEDECIMAL(10,2)才能保证金额计算不出现 0.1 0.2 0.30000000000000004 这种浮点精度问题。这不是教条这是会真实出现的工伤事故。6. 联调与部署环节从本地跑通到服务器上线的完整链路6.1 IDEA 与开发环境里那些不是问题的问题开发环境里最容易让人烦躁的往往不是业务逻辑而是一堆环境层面的小毛病。JDK 环境变量是第一个坎。Java 8 在最新版的 IDEA 里已经不太好使了建议直接上 JDK 17SpringBoot 3.x 要求 JDK 17如果你的 SpringBoot 是 2.xJDK 8 也够用。配置 JAVA_HOME 时候注意Windows 上若环境变量不生效大概率是 Path 里 C:\Program Files\Common Files\Oracle\Java\javapath 这个默认项抢了优先级。解决办法是把%JAVA_HOME%\bin挪到 Path 最前面或者直接把 Oracle 那个项删掉。SpringBoot 版本太高的问题在社区里也经常被提。SpringBoot 3.x 用的是 javax 还是 jakarta 命名空间有大变化Servlet API 从 javax.servlet 迁到 jakarta.servlet很多旧教程和三方库可能没跟上。我建议目前走稳妥路线SpringBoot 2.7.x JDK 8/11 MyBatis非 mybatis-plus这套组合经过大量生产环境验证出问题你搜索解决方案基本上是一搜一个准。如果你非要上 SpringBoot 3.x就得接受配套生态可能踩坑的事实。IDEA 的 MyBatis 插件里我认为最实用的是 MyBatisX——它提供了 Mapper 接口和 XML 之间的快速跳转还能一键生成基本的 CRUD 代码。开发效率能提升不少。6.2 前后端联调时最常碰到的问题联调阶段出的问题十有八九集中在下面三件事跨域问题。虽然我在 Vite 里配了代理解决开发环境的跨域但部署到线上之后前后端如果不在同一个域名下还是会遇到跨域。后端那边我加了全局 CORS 配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意一个细节allowedOrigins(*)和allowCredentials(true)不能同时用浏览器会直接拒绝。得用allowedOriginPatterns(*)来代替这是很多人踩过的坑。时间格式问题。后端返回LocalDateTime前端不处理的话默认是一串带 T 的 ISO 字符串比如2024-06-01T10:30:00很丑也不符合国内习惯。后端在 Jackson 里统一配置date-format为yyyy-MM-dd HH:mm:ss前端拿到就是格式化好的字符串。反过来前端传日期参数给后端时用时间戳或约定的格式传避免歧义。字段命名不一致。前端习惯驼峰后端实体也是驼峰但数据库是下划线。如果你把查询结果直接放到 Map 里返回给前端有些偷懒的写法拿到的 key 就是下划线命名前端模板里怎么点都点不出来。我在代码规范里定了一条所有返回给前端的 VO/DTO 字段一律驼峰命名禁止直接返回 Map。6.3 部署方案Nginx jar 包最后上线我的部署方案是经典的前后端分离部署后端SpringBoot 打 jar 包丢到服务器上nohup java -jar pension-backend.jar --spring.profiles.activeprod 跑起来占用 8080 端口。生产环境配置放application-prod.yml数据库密码通过环境变量注入不写死在代码库里。前端npm run build之后生成 dist 目录丢到 Nginx 的 html 目录下。Nginx 配置里做好两件事一是/路径指向静态文件二是/api路径反向代理到后端 8080 端口server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; # 防止前端路由刷新404 } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html这行非常关键。Vue3 前端用的 history 路由模式如果不配这段用户直接访问http://your-domain.com/person/list这种带路径的地址刷新一次就是 Nginx 404。配了这段之后所有找不到的路径都回退到 index.html让前端路由接管。6.4 线上日志与异常排查后端日志我配了 logback 按天滚动输出异常堆栈单独记到一个文件里。排查线上问题时最常用的命令是tail -200f /logs/pension/error.log日志里出现频率最高的几类异常我提前列在这里遇到不用慌Deadlock found两条更新 SQL 的更新顺序不一致导致死锁。解决思路是让所有事务按相同的顺序更新同一组记录比如先按 person_id 排序后再更新。Data too long for column字段长度不够一般是 real_name 存了 20 个字超了 varchar(20)或传参和表结构对不上。Out of MemoryJVM 堆内存不足先调-Xmx参数如果还不行就排查是不是分了页查全表之类的问题。7. 这个项目后续还能往哪些方向扩展系统做到这个程度已经是一个完整可跑的闭环了。但如果你想让它成为一个更有竞争力的项目尤其是面试作品我建议按下面的方向做增量扩展按投入产出比排序方向一引入 Redis 做缓存与验证码存储。登录验证码、字典数据、用户 Token配合 Redis 实现主动下线都能用 Redis 落地。面试聊天时可以自然引出缓存一致性怎么保证缓存穿透/击穿/雪崩怎么应对这些经典话题。方向二加入消息队列做异步通知。养老金发放成功后通过 MQ 给参保人发送短信通知模拟或者每月缴费计划生成完毕之后异步推送提醒。RabbitMQ 或 RocketMQ 都行。这个点能体现你考虑了系统的高可用和削峰场景。方向三引入定时任务框架。将每月月底自动生成下月缴费计划、每月初给欠费单位发提醒、每年年中做一次待遇基数调整用 Spring 自带的Scheduled或集成 XXL-Job 分布式的都行。如果你在项目里用到了这个面试官基本都会让你展开讲讲任务幂等和失败重试的处理。方向四报表导出增强。用 EasyExcel 做参保缴费明细分页导出、月度统计报表导出这是业务系统里需求最刚性、又能展示代码功力的功能。我做这个项目最大的体会是选型不在多在于贴合场景。养老保险管理系统不是一个热门赛道但它把权限模型、业务流转、复杂查询、数据一致性这些常见的后端核心问题都覆盖了学到的能力完全能平移复用到任何企业管理系统上。这套源码我会继续维护下一步自己也在计划重新梳理一下代码里的注释规范以及把数据库初始化脚本整理得更加傻瓜化——毕竟这套系统最大的意义不是展示技术有多炫而是让拿到源码的人少走两步弯路能更快地把工程跑起来然后专注于学自己想学的那一层。
返回列表