ARTICLE DETAIL

资讯详情

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

基于SpringBoot+Vue3+MyBatis的敬老院管理系统开发实践

基于SpringBoot+Vue3+MyBatis的敬老院管理系统开发实践 先聊点实在的。敬老院管理系统这类项目放在前几年基本就是SSH或者JSPServlet的老古董现在再看主流方案基本已经收敛到SpringBootVue3MyBatis这一套前后端分离组合上了。如果你正在做毕业设计、接外包或者想在公司内部快速搭一个类似的业务管理后台这个项目的技术栈和模块划分非常值得参考。它不只是一个CRUD堆砌的demo而是把老人档案、床位管理、护理记录、费用结算这些真实业务场景全部串了起来前端有Vue3的组件化开发和权限路由后端有JWT鉴权、分页插件、拦截器这些实战中绕不开的东西数据库用MySQL做建模前后端通过RESTful接口对接。这篇文章我就把这个系统的设计思路、核心实现和踩坑经历完整拆开讲一遍尽量做到你看完能直接开干。1. 核心需求拆解与技术选型思路1.1 敬老院管理系统到底在管什么你的读者可能会觉得“敬老院管理系统”就是个简单的信息登记系统但如果真抱着这种想法去做项目做到一半就会非常痛苦。敬老院的业务本质上是一个“照护服务闭环”老人入院前有评估和合同入院后要分床位、建健康档案、排护理计划每天有护理记录每周有健康监测每个月还要算费用、催缴费中间还穿插着家属探访、药品管理、紧急事件上报这么一堆杂事。所以系统的核心模块并不是随意定的而是对着业务线来的老人档案管理基本信息、家属联系方式、健康史、过敏史床位管理楼栋、楼层、房间、床位状态护理管理护理计划、每日护理记录、护理等级变更健康管理血压血糖体温、体检记录、慢病随访费用管理床位费、护理费、餐饮费、医疗费、退款系统管理用户、角色、菜单、操作日志这也是为什么我建议你把数据库表设计当成整个项目的地基。表建得不好后面写Mapper的时候每一条SQL都在给前面还债。1.2 为什么选SpringBootVue3MyBatis这套组合而不是其他先说结论这套组合不是技术上的最优解但它是最“稳妥”的方案尤其是对于毕设和中小型管理系统场景。SpringBoot之所以能成为后端事实标准是因为它把Spring生态里最繁琐的配置全部自动化了。你不需要再写一堆XML配置Bean不需要手动配事务管理器一个spring-boot-starter-web就带起内嵌Tomcat和MVC框架。配合spring-boot-starter-validation、spring-boot-starter-security或者轻量的JWT方案基本上能把一个管理系统的后端骨架在半小时内搭完。Vue3这边相比Vue2最大的变化是Composition API。对于管理系统这种大量“表格表单弹窗”的页面模式setup语法糖配合ref、reactive、computed写起来逻辑聚拢、复用方便比Options API舒服太多。再加上Vite的秒级热更新开发体验完全是另一个档次。如果你之前用过Vue2那重点要适应的是响应式原理的变化——ref需要.value访问reactive只支持对象和数组还有watch和watchEffect的触发时机差异。MyBatis的选择可能是很多人纠结的点——半自动ORMSQL要自己写感觉不如JPA省事。但管理系统恰恰是MyBatis发挥最好的场景。因为业务复杂SQL往往不是简单的单表查询而是多表关联、聚合统计、动态条件拼接。用MyBatis的where、if标签可以非常灵活地搞定这些而且SQL是显式可控的出问题的时候你直接拿SQL去数据库跑一遍就知道怎么回事了。JPA在简单CRUD上确实省事但碰到复杂查询JPQL和SQL之间的心智切换成本很高。MySQL就不用多说了主流管理系统首选。关键是要注意版本和编码5.7之后默认字符集才是utf8mb4装的时候一定要确认character_set_server否则中文表情包或者生僻字直接变问号。1.3 前后端分离不等于两个项目分开跑前后端分离这个词被说烂了但很多人在实际开发中还是会踩坑。前后端分离不仅仅是前端一个项目、后端一个项目而是两者通过HTTP接口通信各司其职。前端只负责渲染和交互后端只负责业务逻辑和数据存取。好处是非常现实的前端可以用Vite起一个8080端口的开发服务器后端SpringBoot跑在8080互不干扰热更新互不影响。部署的时候前端打包成静态文件丢到Nginx后端打成Jar包用systemd或者Docker守护扩展时候还可以把静态资源和接口服务拆到不同的机器上。但坏处也很明显——跨域、鉴权、联调这些麻烦事就来了。跨域要配CORS或者Nginx反向代理鉴权要用Token而不是Session接口要统一返回格式方便前端处理。这些都不是技术难题但是会在开发过程中反复出现下面我会一一把它们拿出来详解并附上能直接跑的代码。2. 数据库设计把老人档案、床位与费用这三条主线串起来2.1 老人档案与家属信息怎么建模才够用数据库设计是这个项目里最值得花时间的一环。如果建表的时候只是随意列几个字段后面写业务代码就会很虐。我见过不少人把老人联系方式直接跟家属联系方式放在同一个elder表里看似方便实际上一旦一个老人有多个子女或者家属信息需要单独维护时这种设计就凉了。合理的做法是拆成两张表elder老人主表存姓名、性别、身份证号、出生日期、入住时间、护理等级、床位ID、状态在住/退住/待入住等。family_member家属表存老人ID、姓名、关系、联系电话、是否紧急联系人。为什么要拆因为一个老人可以对应多个家属而且家属信息变更的频率比老人基本信息高得多。如果都在一张表里要么冗余存储要么只能记录一个人都不合理。这里的设计原则叫“一对一或一对多关系单独建表”在管理系统里非常常用。老人表的字段里有两点值得提醒。第一身份证号要加唯一索引这不仅是为了防重复也是入住登记时判断“老人是否已经登记过”的最快方式。第二不要直接把年龄作为一个字段存而是存出生日期需要的时候用SQL的TIMESTAMPDIFF(YEAR, birth_date, CURDATE())去算。年龄每年都在变存字段意味着每年都要执行批量更新完全没有必要。2.2 床位管理状态流转是重点床位表的设计其实不复杂核心字段就是楼栋、楼层、房间号、床号、床位类型单人间/双人间/多人间、状态空闲/已入住/维修/预留。但这个模块的真正难点在于状态流转老人入住时床位从空闲变成已入住退住时再释放为空闲维修时要标记为维修状态预留时不能安排其他老人。这个流转不该散落在业务代码里最好在Service层做一个统一的方法控制比如checkInElder()里先校验床位状态、再更新床位状态、最后创建老人入住记录三个操作放在一个事务里。任何一步失败整体回滚。这里我踩过一个坑当初为了省事直接把床位状态写在elder表的case里入住退住的时候手动UPDATE。后来发现一旦某个功能漏了更新床位状态系统里就会出现“老人已入住但床位还是空闲”的数据错乱。后来我把床位状态变更收敛到一个BedService里所有业务操作必须走这个Service才彻底解决。2.3 护理记录与健康档案的时间维度护理记录和健康档案这两个模块有一个共同点它们都是强时间序列数据。也就是说一条记录对应一个时间点你通常查询的是“某老人某段时间内的所有记录”而不是某个单条记录的详情。所以这两张表必须建好时间相关的索引CREATE TABLE care_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_id BIGINT NOT NULL, caregiver_id BIGINT NOT NULL, care_type VARCHAR(50) COMMENT 护理项目翻身/喂饭/洗澡/口腔护理等, content VARCHAR(500), record_time DATETIME NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_elder_time (elder_id, record_time) );注意最后这行INDEX idx_elder_time (elder_id, record_time)这是这个表最重要的索引。没有它当你查询“某个老人最近一个月的护理记录”时MySQL只能全表扫描数据量一到十万条级别就会明显变慢。这与选什么ORM无关是数据库基本原理。健康档案表类似除了主键之外建议把elder_id和measure_time做成联合索引因为绝大多数业务都是按老人和时间范围查的。血压、血糖、心率这些指标可以做成字段也可以做成分组明细表取决于你是想“快速展示最近一次记录”还是“分析变化趋势”。养老院场景两者都有我的建议是主表存最新记录明细表存历史趋势两个表通过elder_id关联。2.4 费用模块的数据结构账单流水双保险费用模块是最容易设计失衡的地方。很多初学者会把费用设计成一张大表字段就是床位费、护理费、餐饮费、医疗费……每个老人一行记录定期更新。这种设计在月度账单出来的时候很好用但一旦遇到部分退费、跨月调整、支付方式不同就会让人抓狂。更稳的做法是“账单流水”双层结构fee_bill月度账单表每个老人每个月生成一条记录总金额、已收金额、应收金额、状态待支付/已结清/已退款。fee_flow费用流水表每发生一笔费用就记录一条字段包括老人ID、账单ID、费用类型、金额、方向收入/支出、发生时间、操作人。这样做的核心价值是“数据可追溯”。你要回答“这个月总共收了多少钱”就看账单表要回答“这个月张奶奶为什么费用多了300块”就查流水表。而且账单表可以冗余汇总字段比如total_amount流水表则保存每一笔明细。查询时先走账单表拿汇总点进去再查流水表看明细性能好逻辑也清晰。这种设计模式同样适用于库存管理、订单系统凡是涉及到“汇总明细”的业务都可以参照这种两层结构。你把这个思路写在项目文档里面试官问起来会非常加分。3. 后端核心实现SpringBoot的配置与业务落地3.1 项目初始化与yml配置创建SpringBoot项目我用的是Spring Initializr选Java 8或者Java 11都行如果你用的是更高版本JDK建议至少17往上8的高版本JDK在某些IDE里适配会有小问题依赖加上Spring Web、MyBatis Framework、MySQL Driver、Lombok、Validation。application.yml里的配置是门面活很多人上来就写得乱七八糟。这里给你一个我项目里验证过的基础版本server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/elder_home?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你自己的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.elder.entity configuration: map-underscore-to-camel-case: true logging: level: com.elder.mapper: debug几个容易出问题的点我重点说第一MySQL驱动类名。8.x版本用的是com.mysql.cj.jdbc.Driver带不带cj区别很大写错的直接启动报ClassNotFound。5.x版本则是com.mysql.jdbc.Driver别搞混。第二serverTimezoneAsia/Shanghai必须加。服务器时间是中国时区数据库也是驱动默认却用UTC不加的话你会发现时间整整差了8小时。排查这类问题非常费时配置里直接带上最省心。第三map-underscore-to-camel-case: true必须开启。数据库字段是下划线命名care_typeJava实体是驼峰命名careType没有这个配置MyBatis查出来的字段就全为null。这个配置对开发效率的提升非常明显不用手写resultMap到吐血。第四logging.level.com.elder.mapper: debug是在开发阶段把SQL打印到控制台。MyBatis虽然自带日志但不配这个你看到的只有一堆参数没有完整的SQL语句。配上后每条SQL的执行和参数都会很直观地展示出来定位问题效率翻倍。3.2 JWT登录鉴权无状态登录的正确姿势管理系统肯定绕不开登录鉴权我用的是JWT加拦截器的组合方案没有上Spring Security——因为对于这种单一后台系统Spring Security的复杂度超出了实际需要写起来反而啰嗦。核心思路是用户登录成功后服务端生成一个Token返回给前端。前端存到localStorage里每次请求在Authorization头带上Token。后端用一个拦截器拦截除了/api/auth/login之外的所有请求校验Token是否有效。登录接口的核心代码长这样PostMapping(/api/auth/login) public Result login(RequestBody Valid LoginRequest req) { // 1. 根据用户名查用户 SysUser user userMapper.findByUsername(req.getUsername()); // 2. 校验密码 这里注意要用MD5或者BCrypt不能明文 if (user null || !DigestUtils.md5DigestAsHex(req.getPassword().getBytes()).equals(user.getPassword())) { return Result.error(用户名或密码错误); } // 3. 生成JWT String token JwtUtil.createToken(user.getId(), user.getUsername()); return Result.success(Collections.singletonMap(token, token)); }密码加密强烈建议用BCryptPasswordEncoder比MD5安全很多。我为了演示所以简化了正式项目千万别用明文或者裸MD5。拦截器里校验Token的写法也很关键Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); // 判断是否以 Bearer 开头 if (token ! null token.startsWith(Bearer )) { token token.substring(7); Claims claims JwtUtil.parseToken(token); if (claims ! null) { request.setAttribute(userId, claims.get(userId)); return true; } } response.setStatus(401); return false; } }记得在WebMvcConfigurer里注册这个拦截器并通过excludePathPatterns放行登录接口和静态资源。这一步漏了前端所有请求都会401。3.3 MyBatis分页插件从配置到复杂查询MyBatis的分页插件基本是管理系统的标配因为表格查询不可能一次把全部数据查出来必须分页。我用的是PageHelper也是在springboot项目里最成熟的选择。Maven引入依赖后配置一行就可以生效PageHelper.startPage(pageNum, pageSize); ListElderVO list elderMapper.selectElderList(query); PageInfoElderVO pageInfo new PageInfo(list);这段代码的核心在于PageHelper.startPage()用的其实是ThreadLocal所以它必须紧跟要分页的那条Mapper查询语句。如果中间插入了别的查询逻辑、或者调用了其他的Mapper方法泛灵论就来了分页可能作用到错误的SQL上。这是一个非常经典的坑怎么排查我后面会在问题清单里详细说。实际业务查询通常不止一张表分页查询需要返回带关联信息的列表比如查“老人列表”时要显示床位号、护理等级、家属联系方式。这里推荐写SQL来JOIN而不是查出老人列表后再循环查家属表N1问题。一个典型的列表查询Mapper XML如下select idselectElderList resultTypecom.elder.entity.vo.ElderVO SELECT e.id, e.name, e.gender, e.birth_date, e.status, b.building_name, b.room_no, b.bed_no, (SELECT f.name FROM family_member f WHERE f.elder_id e.id AND f.is_emergency 1 LIMIT 1) AS emergency_name, (SELECT f.phone FROM family_member f WHERE f.elder_id e.id AND f.is_emergency 1 LIMIT 1) AS emergency_phone FROM elder e LEFT JOIN bed b ON e.bed_id b.id where if testkeyword ! null and keyword ! AND (e.name LIKE CONCAT(%, #{keyword}, %) OR e.id_card LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null and status ! AND e.status #{status} /if /where ORDER BY e.create_time DESC /select这样可以一次性把列表页要展示的数据全部查出来不用在Java里做二次查询。分页插件只需要处理Limit和Count查询逻辑保持原样就能很好配合。3.4 事务与接口参数校验别放到后头再说管理系统的写操作必须加上事务控制。最直接的方式就是在Service方法上加Transactional这是Spring最朴素也最可靠的事务方式。举个例子老人办理入住这个操作至少包含三件事Transactional public void checkIn(ElderCheckInDTO dto) { // 1. 校验床位是否空闲 Bed bed bedMapper.selectByIdForUpdate(dto.getBedId()); if (!空闲.equals(bed.getStatus())) { throw new BusinessException(该床位不可用); } // 2. 更新床位为已入住 bedMapper.updateStatus(dto.getBedId(), 已入住); // 3. 创建老人档案 Elder elder new Elder(); // ... 填充字段 elderMapper.insert(elder); }中间任何一个步骤抛异常事务回滚床位和老人档案都不会留下脏数据。需要特别提醒的是selectByIdForUpdate这个细节在并发场景下两个管理员同时给同一个床位办理入住如果没有行锁两个人都能查到床位是空闲的然后都执行更新数据就坏了。FOR UPDATE会对这一行加锁第二个请求只能等第一个事务提交后读取这时候床位状态已经是“已入住”就会正确报错。接口参数校验这块我习惯用Validated加JSR 303注解。实体字段上标NotBlank、NotNull、Pattern接口上用Valid RequestBody参数不合格直接抛MethodArgumentNotValidException然后在全局异常处理器里统一返回提示信息。别在Controller里手写一串if判断参数是否为空又丑又容易漏。4. Vue3前端架构从初始化到核心页面落地4.1 Vite Vue3 Element Plus 的项目初始化前端我建议直接用Vite来新建项目。相比WebpackVite最大的优势是开发服务器的冷启动速度和热更新速度这对高频改代码的管理系统开发来说体验是决定性的。npm create vitelatest elder-admin -- --template vue cd elder-admin npm install装好基础框架后再把依赖补上npm install element-plus element-plus/icons-vue axios vue-router piniaElement Plus是Vue3生态里最成熟的中后台UI组件库表格、表单、弹窗、日期选择器这些管理系统的“基建”它都做得比较完善可以直接拿来用。如果你对组件主题有定制的需求它支持SCSS变量覆盖改动也还算方便。Vue3项目里一个比较重要的心智转换是Composition API。过去Vue2里我们写data、methods、computed是分散的选项现在用setup语法可以把一个页面的所有逻辑聚合在一个函数里。比如一个列表页分页数据、搜索表单、加载方法都能放在一起script setup import { ref, onMounted } from vue import { getElderList } from /api/elder import { ElMessage } from element-plus const loading ref(false) const list ref([]) const total ref(0) const query ref({ pageNum: 1, pageSize: 10, keyword: , status: }) async function loadList() { loading.value true try { const res await getElderList(query.value) list.value res.list total.value res.total } finally { loading.value false } } function handleSearch() { query.value.pageNum 1 loadList() } function handleReset() { query.value { pageNum: 1, pageSize: 10, keyword: , status: } loadList() } onMounted(loadList) /script这段代码虽然简单但它体现了Composition API的核心价值——功能逻辑以“函数”为单位聚合而不是分散在多个选项里。你要把这段逻辑复用到其他页面直接把这个setup里的部分提取成useElderList这个自定义Hook就行。4.2 Axios封装与请求拦截器管理系统的前端开发离不开Axios但如果每个页面都直接调axios.get等接口多起来之后就会很痛苦。必须在项目早期就封装好一个统一的请求模块。// src/api/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截器自动带上token 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 // 约定后端返回格式为 { code, data, message } if (res.code ! 200) { ElMessage.error(res.message || 请求失败) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) router.push(/login) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default request这段封装的思路非常关键前端所有请求自动携带Token后端返回的401统一做跳转处理不用在每个页面里重复写错误提示逻辑。这样后端的接口只需要返回数据和状态码前端模块只需要关心业务成功的情况。所有接口也建议集中在src/api目录下面按模块拆分文件。比如elder.js里定义老人的CRUD接口care.js里定义护理记录接口fee.js里定义账单接口。这样后端的接口变更时前端只需要改对应的API文件不需要全局搜索每个页面里的请求地址。4.3 表格页面的开发套路搜索分页操作列管理系统里最典型的页面就是“表格搜索条件分页操作列”这个套路一旦熟悉了后面加任何新模块都是复制粘贴再改改的事。我用Element Plus的表格组件来做示例template div classpage-container el-card classsearch-card el-form :inlinetrue :modelquery el-form-item label老人姓名 el-input v-modelquery.keyword placeholder姓名/身份证号 clearable keyup.enterhandleSearch / /el-form-item el-form-item label状态 el-select v-modelquery.status clearable placeholder全部 el-option label在住 value在住 / el-option label退住 value退住 / /el-select /el-form-item el-form-item el-button typeprimary clickhandleSearch查询/el-button el-button clickhandleReset重置/el-button /el-form-item /el-form /el-card el-card div classtable-toolbar el-button typeprimary clickopenForm()新增老人/el-button /div el-table v-loadingloading :datalist border stripe el-table-column propname label姓名 width120 / el-table-column propgender label性别 width80 / el-table-column propage label年龄 width80 / el-table-column proproomNo label房间号 width100 / el-table-column propbedNo label床号 width80 / el-table-column propcareLevel label护理等级 width120 / el-table-column propstatus label状态 width100 template #default{ row } el-tag :typerow.status 在住 ? success : info{{ row.status }}/el-tag /template /el-table-column el-table-column label操作 min-width220 fixedright template #default{ row } el-button typeprimary link clickopenForm(row)编辑/el-button el-button typesuccess link clickopenDetail(row)详情/el-button el-popconfirm title确认删除该老人档案吗 confirmhandleDelete(row) template #reference el-button typedanger link删除/el-button /template /el-popconfirm /template /el-table-column /el-table el-pagination v-model:current-pagequery.pageNum v-model:page-sizequery.pageSize :totaltotal :page-sizes[10, 20, 50, 100] layouttotal, sizes, prev, pager, next, jumper size-changeloadList current-changeloadList / /el-card /div /template这段模板基本涵盖了管理系统列表页的全部交互搜索条件、重置、数据加载、操作列、删除确认、分页。写熟这个套路之后你上手任何一个后台管理项目都会非常快。这里有个小细节删除操作我用了el-popconfirm做二次确认这比浏览器自带的confirm弹窗美观得多而且不会打断用户的视觉流。这在管理系统里属于体验细节但用户感知非常明显。4.4 路由守卫与动态侧边栏菜单管理系统几乎都有权限需求但不同角色看到的菜单不一样。简单方案就是在前端根据用户角色来控制菜单显示但更实用的是让后端返回菜单列表前端动态生成路由和侧边栏。动态路由的核心难点在于Vue Router的addRoute方法它可以在运行时动态注册路由。实现思路是登录后从后端拿到当前用户的角色和菜单列表在路由守卫里判断如果还没有动态添加过路由就根据菜单数据调用router.addRoute()全部路由添加完成后再放行到目标页面路由守卫的核心逻辑如下// src/router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token) { if (to.path /login) { next() } else { next(/login) } return } // 已登录访问登录页跳首页 if (to.path /login) { next(/) return } // 如果还没有动态菜单就先拉取菜单再放行 if (!useMenuStore().loaded) { useMenuStore().fetchMenus().then(() { next({ ...to, replace: true }) }) return } next() })这样每次刷新页面后动态路由都会重新生成一遍刷新不会白屏。这个逻辑一开始容易漏如果发现刷新后就404多半就是动态路由没有在守卫里正确处理。关于侧边栏如果项目用的是Element Plus可以直接用el-menu配合el-sub-menu和el-menu-item递归渲染菜单树。Vue3的递归组件写法比较自然一个组件自己调用自己就能渲染出无限层级的菜单结构这个也建议抽成一个单独的SidebarItem.vue组件。4.5 表单弹窗与校验管理系统里另一个高频场景是“表单弹窗”新增和编辑共用同一个弹窗。实现上要注意区分提交时的接口。script setup const dialogVisible ref(false) const formRef ref(null) const formData ref({ id: null, name: , gender: 男, birthDate: , phone: , bedId: null, careLevel: 自理 }) const rules { name: [{ required: true, message: 请输入姓名, trigger: blur }], phone: [ { required: true, message: 请输入联系电话, trigger: blur }, { pattern: /^1[3-9]\d{9}$/, message: 手机号格式不正确, trigger: blur } ], bedId: [{ required: true, message: 请选择床位, trigger: change }] } function openForm(row) { if (row) { formData.value { ...row } } else { formData.value { id: null, name: , gender: 男, birthDate: , phone: , bedId: null, careLevel: 自理 } } dialogVisible.value true } async function handleSubmit() { await formRef.value.validate() const api formData.value.id ? updateElder : addElder await api(formData.value) ElMessage.success(formData.value.id ? 修改成功 : 新增成功) dialogVisible.value false loadList() } /script我特意在openForm里区分了新增和编辑两个逻辑有row就走编辑回填没有row就走新增默认值。提交的时候根据是否有id决定调哪个接口。这比开两个弹窗、写两套表单省事得多。表单校验的规则要注意手机号的pattern校验写错了会出现“明明号码是对的却提示格式错误”的问题。正常手机号是第二位3-9的11位数字所以正则写^1[3-9]\d{9}$这个在管理系统里基本适用。5. 高频问题排查跨域、缓存、分页失效与部署坑5.1 跨域问题CORS和Nginx两种解法前后端分离项目跨域问题基本是避不开的第一道坎。前端跑在5173Vite默认端口后端跑在8080两者端口不一致浏览器就会拦截跨域请求。后端的解法是在SpringBoot里加上CORS配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这串配置的意思是后端允许所有来源跨域访问允许GET/POST/PUT/DELETE这四种方法允许携带凭证也就是Cookie或者Authorization头预检请求缓存1小时。不过更推荐的做法是部署时用Nginx反向代理把/api前缀的请求转发到Java服务前端请求的地址就是同源地址根本不会触发跨域location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里要注意proxy_pass末尾的/带不带是有讲究的。不带/会把原始的/api前缀也传过去带/则会用/api替换成空前缀。如果你的后端接口本身就不带/api那这里要用带/的写法同时前端axios的baseURL也要相应调整。5.2 MyBatis缓存与Update执行慢的问题MyBatis自带的缓存分为一级缓存和二级缓存。一级缓存默认开启是SqlSession级别的同一个会话里的相同查询会复用结果。但这在Spring集成的情况下经常出问题因为Spring管理的SqlSession可能跟预期的不一样导致“第一次查出的数据更新后第二次查询还是旧数据”。最稳妥的做法是在关键查询的Mapper XML里显式设置useCachefalse或者干脆对表更新频繁的业务关闭二级缓存避免脏读。很多人在网上搜“mybatis的Update执行慢”其实排查下来大部分原因不是SQL本身慢而是忘了在关联查询时加索引、或者查询条件里用了函数导致索引失效。比如下面这种UPDATE elder SET status #{status} WHERE DATE(create_time) CURDATE()DATE(create_time)这个写法很常见但它会让create_time上的索引失效更新全表扫描。正确写法是UPDATE elder SET status #{status} WHERE create_time CURDATE() AND create_time DATE_ADD(CURDATE(), INTERVAL 1 DAY)这样不仅能用到索引语义也更清晰。5.3 分页插件“失效”的典型场景PageHelper失效的案例我见得太多最典型的就是PageHelper.startPage(1, 10)之后紧接着的不是Mapper查询语句而是中间穿插了其他逻辑。比如PageHelper.startPage(pageNum, pageSize); ListElder list adminService.listAdmins(); // 这里执行的是别的Mapper查询 ListElderVO result elderMapper.selectElderList(query); // 分页作用到这里就失效了因为PageHelper.startPage()是基于ThreadLocal的它只能作用于同一个线程里下一次执行的查询中间如果没有被下一次查询“消耗”掉它就会一直挂在ThreadLocal上如果线程复用比如Tomcat线程池甚至会发生分页数据错乱。解决办法是让startPage和Mapper查询紧紧挨在一起中间不要插入任何其他查询逻辑。如果必须在分页查询前后执行其他查询建议拆成两个Service方法或者在查询前先PageHelper.clearPage()清除分页上下文。5.4 MySQL时区与乱码问题MySQL装好后如果发现查询的时间不对比实际时间少8小时99%是时区问题。要么在连接串里加上serverTimezoneAsia/Shanghai要么在MySQL配置文件里设置default-time-zone 08:00乱码问题则基本是字符集问题。建库的时候就要指定CREATE DATABASE elder_home DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;特别注意utf8mb4和utf8的区别。MySQL里utf8实际不是完整的UTF-8编码它最多只能存3个字节但Emoji和一些生僻字需要4个字节所以强烈建议统一用utf8mb4。建表的时候如果没跟上后面出了问题再去改字符集表数据很大时会非常头疼。5.5 前端部署后刷新404的问题Vue3用history模式路由时在前端部署有个经典问题用户访问/没问题但一旦刷新/elder/list这样的二级页面Nginx会404。这是因为前端路由在Nginx层面并不存在对应的真实文件Nginx默认会去找/elder/list.html这个文件找不到就返回404。解决方案是让Nginx把所有的路由请求都回退到index.htmllocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }关键就是try_files $uri $uri/ /index.html;这一行它的意思是先尝试直接访问路径对应的文件如果找不到就全部转发到index.html再由前端路由接管。这是Vue3 history模式部署的必经之路很多人第一次部署都会在这里卡住。6. 部署上线的两种姿势与个人建议6.1 传统方式Nginx Jar包后端打成Jar包后用java -jar elder-admin.jar就能跑。但生产环境不能这样裸跑需要用systemd或者nohup守护进程保证服务挂了能自动拉起。一个简单的systemd服务配置[Unit] DescriptionElder Home Admin Afternetwork.target [Service] Userroot WorkingDirectory/opt/elder-admin ExecStart/usr/bin/java -Xms256m -Xmx512m -jar /opt/elder-admin/elder-admin.jar Restartalways RestartSec10 [Install] WantedBymulti-user.target前端打出来的静态文件放到Nginx的HTML目录再配上前面说的try_files规则和一个/api反向代理一个基本可用的生产环境就出来了。6.2 进阶方式Docker Compose一键编排如果服务器上还装了MySQL不建议跟SpringBoot应用混装在一台机器上手动管理。用Docker Compose可以把MySQL、后端、前端三个服务编排起来一条docker compose up -d全部搞定。这个对后期部署、迁移、扩容都有极大的便利。如果你打算把项目写成简历项目或毕设亮点Docker部署算得上一个不错的加分项面试官通常对“会用容器化部署”比较认可。但注意一定要写清楚镜像的构建方式和数据卷的挂载路径否则面试官追问起来容易露馅。6.3 关于源码本身的一点掏心窝子的话这类管理系统源码在网上能找到不少但质量良莠不齐。我的建议是不要只看重“能跑起来”就结束了也不要拿来直接交作业。真正有价值的是把源码里的表结构设计思路、权限控制方案、分页查询实现这些底层逻辑吃透然后根据自己的理解重构一部分代码。比如把你的系统里加一个“用药提醒模块”或者“老人定位手环对接”这个增量开发的过程才是面试或答辩时你能侃侃而谈的部分。毕竟系统的价值在于业务理解和技术实现的结合。数据结构建得合理、业务闭环完整、代码没有明显安全漏洞这套系统对你的价值就远超一个毕设分数本身。
返回列表