前后端分离实战:SpringBoot+Vue+MyBatis公寓管理系统 前后端分离这个词大家应该都不陌生但能把一个完整项目从需求拆分、数据库设计、后端接口、前端页面到服务器部署全程走通的其实并不多。我早期也在这上面摔过跤跟着残缺的教程抄了大半天最后数据库表都连不上。这次借着夕阳红公寓管理系统这个项目我把整套开发流程从头到尾完整复盘了一遍每一步踩的坑、每一个关键配置都记录下来了希望给准备做类似管理系统或者正在做毕业设计的同学一些实在的参考。这个系统的定位很清晰就是给养老公寓做一套数字化管理工具。公寓里有老人、有护工、有管理人员还有老人的家属得把老人基本信息、入住房间、健康记录、护工分配、收费退费这些事情全部管起来。后端用的SpringBootMyBatis前端用的Vue全家桶数据库是MySQL整套前后端分离架构算是在Java技术栈里最实用、最常被问到的一套组合。1. 项目整体拆解夕阳红公寓管理系统到底管什么1.1 核心需求分析别看是个管理系统需求拆起来远比想象中复杂。首先要搞清楚这个系统里有哪几类人也就是角色。我最初做需求梳理的时候先画了一张角色与权限的草图不会画太复杂但必须能回答一个问题谁能够做什么事。在这个系统里我把角色划分成三大类超级管理员、公寓员工包括护工和前台、老人家属。公寓员工还细分为管理员角色和普通护工角色。这个权限设计直接决定了后面接口的鉴权逻辑。刚开始做的时候我也想过不搞角色区分一切从简但后来一想老人家属只会看自己老人的健康信息和缴费账单不能让他看到其他老人的数据甚至不能看到公寓的运营数据。这个权限边界必须卡死。功能模块上我最终整理出这几块老人档案管理入住、退住、档案编辑、状态查询房间管理房间分配、调房、退房、房间状态维护健康管理血压、心率、用药提醒、体检报告记录护工管理护工信息、排班计划、服务任务分配费用管理入住缴费、月度服务费、退款结算家属管理家属账号绑定老人、留言反馈系统管理用户管理、角色权限、操作日志每一块都是一个独立业务模块之间又有关联。比如老人入住了房间房间状态要变成已占用老人退住了费用模块要能生成退款单。这些业务联动关系在数据库设计阶段就要考虑清楚否则代码写到一半再来改表结构那真的太痛苦了。1.2 功能模块与页面结构规划模块划分清楚后就可以直接映射到前端页面了。我习惯在开发前先列一张页面清单这样可以避免前后端开发过程中反复改路由。夕阳红公寓管理系统的前端页面我做了如下规划登录页账号密码登录角色跳转首页仪表盘统计卡片展示老人总数、在住人数、空闲房间数、本月收入老人管理页列表、搜索、新增、编辑、详情抽屉、退住操作房间管理页楼栋房号维度管理房间状态色块展示健康管理页按老人查看健康记录时间线新增健康指标护工管理页护工档案、排班日历费用管理页费用明细列表、新增缴费单、退款单家属管理页绑定老人、查看留言页面清单出来以后前后端接口也就心中有数了。这个做法的好处是后续开发不必一边想页面一边想接口专注度会高很多。1.3 角色权限边界权限边界是整个系统的核心我在实际开发里是这么处理的。后端的拦截器负责接口层面的权限校验前端路由通过路由守卫控制页面访问。双端拦截是为了防止有人绕过页面直接调接口。接口层面的权限控制其实不难核心就是给每个接口定义允许访问的角色列表。表结构上我预留了角色表和用户角色关联表虽然项目里角色数量不多但用表结构来管理比写死在代码里要好维护得多。后续如果公寓要增加新的角色比如康复师、营养师直接插一条角色记录再绑定菜单权限就可以了。角色权限边界一览操作模块超级管理员公寓管理员护工家属老人档案增删改查增删改查查看更新仅查看本人房间管理全部操作全部操作查看状态不可见健康记录查看导出增删改查增删改查仅查看本人费用管理全部操作新增查询不可见查看本人账单家属管理全部操作绑定管理不可见本人留言这个表格是我在开发前反复调整后的结果。权限这块一定不能省就算这个项目只是一个课程设计或者毕业设计完善的权限控制也是评分时很加分的点。2. 技术选型解析为什么是SpringBootVueMyBatisMySQL2.1 后端的三个关键决策先说说SpringBoot。我最早接触Java Web的时候还在用SSH那套配置多到怀疑人生。SpringBoot最直接的感受就是把配置简化了内嵌了Tomcat一个jar包跑起来不用单独装服务器这对本地开发和后续部署都很友好。我选SpringBoot 2.x版本原因很简单稳定性好第三方框架兼容性佳网上资料也足够多。MyBatis这块要展开说一下。ORM框架里JPA和MyBatis是两大阵营JPA确实封装度高但SQL控制力弱而且对于国内很多业务系统来说复杂的多表关联查询是家常便饭。MyBatis把SQL写在XML里每一句SQL都是自己掌控的出了问题也容易排查。当然我用的是MyBatis-Plus这个增强工具单表操作不用写SQL直接调用封装好的方法多表查询再写XML。很多人纠结MyBatis和MyBatis-Plus的关系其实我的判断很简单如果你要快速开发用Plus可以显著减少样板代码如果你希望完全掌控SQL那就用原生MyBatis。我在这个项目里选的是MyBatis-Plus因为既要追求开发效率又要保留SQL的可控性。2.2 前端框架的选型理由Vue在国内的普及度就不用多说了。2.x版本配Element UI这是最经典的组合生态成熟、中文文档全、组件丰富遇到问题一搜就有答案。Vue 3我也了解过但考虑到这个项目要和大量后端同学配合大家更熟悉Vue 2的写法所以还是优先考虑兼容性。如果你自己一个人从零写用Vue 3体验也不差但团队协作或者参考现有代码的话Vue 2反而更方便。前端我用Vue CLI搭建项目配合Vue Router做路由管理Axios做HTTP请求状态管理用Vuex不过只在登录信息和当前用户状态时用。这套组合是极其主流的前端技术栈即使以后做更复杂的项目这套基础也不会白学。2.3 数据库和工具链MySQL 8.0是当前的主流版本性能更好窗口函数这些功能也有。需要特别注意的是MySQL 8.0默认的认证插件是caching_sha2_password有时候老版本的连接驱动会不兼容必须确保引入的mysql-connector-java版本是8.x以上。这个问题我遇到不止一次了很多同学项目启动报错查半天最后发现是驱动和数据库版本不匹配。开发工具方面后端我用IntelliJ IDEA前端用VS Code数据库可视化用Navicat 16。如果不想用收费软件也可以用免费的DBeaver体验也很不错。这些工具的选择完全看个人习惯不一定要和我一样顺手最重要。2.4 前后端项目目录结构一个好的项目结构能让代码维护事半功倍。后端的包结构我按功能模块划分而不是按技术类型划分。采用模块化包结构每个业务模块下包含Controller、Service、Mapper三层。这样的好处是改一个模块只需要在对应的包下面操作不会东翻西找。前端目录结构也类似按照页面模块组织src/ ├── api/ // 接口请求封装 ├── assets/ // 静态资源 ├── components/ // 通用组件 ├── router/ // 路由配置 ├── store/ // 状态管理 ├── views/ // 页面视图 │ ├── login/ │ ├── dashboard/ │ ├── elderly/ │ ├── room/ │ ├── health/ │ ├── caregiver/ │ ├── fee/ │ └── family/ └── utils/ // 工具函数3. 数据库设计与核心表结构3.1 核心表设计总览数据库是整个系统的根基表结构设计得合理后面写代码会十分顺畅设计得不好后面每次联调都是在填坑。我设计的表一共有10张这里挑几张核心的给大家详细拆解。数据库名我取名为sunset_apartment字符集用utf8mb4排序规则utf8mb4_unicode_ci。utf8mb4这个点非常重要如果不小心用了utf8遇到表情符号或者生僻字就会乱码。MySQL的utf8其实并不是真正的UTF-8它最多只能存3个字节的字符而utf8mb4才是真正的4字节UTF-8。这个坑特别隐蔽插入数据成功了但显示乱码排查半天才发现是字符集的问题。核心表清单如下表名说明关键字段user用户表id, username, password, roleelderly老人档案表id, name, idcard, gender, statusroom房间表id, building, room_no, statushealth_record健康记录表id, elderly_id, blood_pressure, heart_ratecaregiver_task护工任务表id, caregiver_id, elderly_id, task_datefee_record费用记录表id, elderly_id, fee_type, amount, statusfamily_user家属表id, user_id, elderly_id, relationcheck_in入住记录表id, elderly_id, room_id, check_in_timesystem_log操作日志表id, user_id, action, operation_timemenu菜单权限表id, menu_name, path, role3.2 老人档案表与房间表设计老人档案表是整个系统的核心主表字段最多也最需要花心思。除了身份证号、姓名这些基础字段我额外加了入住状态字段用0和1表示在住和退住这个字段在业务统计里非常常用。首页仪表盘要实时统计在住人数查这个字段就可以了不用关联入住记录表。老人年龄我建议用出生日期来计算不要在表里直接存年龄因为老人每年都会长一岁存死数字容易出错。房间表我设计了楼栋号和房号用building和room_no两个字段组合比如3栋408。房间状态字段特别需要注意我用0表示空闲1表示已入住2表示维修中。这几个状态之间的流转都是有业务逻辑的房间维修期间不能分配老人如果有人在住也不能把房间标记为维修。这个状态机的逻辑我在后端的房间管理接口里做了校验防止有人绕过页面直接调用接口把状态改成非法值。3.3 健康记录表与费用表设计健康记录表我设计成了一条时间线的模式。每次量血压、心率就新增一条记录。查询的时候按老人ID和时间倒序排列前端展示的时候用时间线组件渲染看起来非常直观。这个表还设置了用药提醒字段虽然现阶段没有做消息推送但预留了这个字段后续如果要对接短信推送就非常方便。费用记录表相对简单但有一个字段设计上要想清楚费用类型。我用费用类型区分入住费、月服务费、餐费、医疗费、退款。金额字段要注意数据库里用decimal类型不要去用float或者double因为二进制浮点数是无法精确保存的金额计算会出现0.10.2不等于0.3这种情况。用decimal可以规避这个问题。3.4 数据库连接的关键配置数据库建好之后后端要正确连接MySQL必须把配置写对。我在application.yml里的配置如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sunset_apartment?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.sunset.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有几个非常容易踩的坑我特意标注一下。useSSLfalse必须加否则本地开发环境没有SSL证书连接会一直卡住或者报警告。serverTimezoneAsia/Shanghai必须加否则MySQL 8.0连接时区不一致会直接报错之前的版本默认使用服务器的时区有时候能启动但日期查询全是错的。allowPublicKeyRetrievaltrue在某些版本的MySQL驱动下第一次连接会报Public Key Retrieval is not allowed错误如果不加就需要手动配置密钥认证。这四处配置都是前人踩坑总结出来的直接照抄都没问题。4. 后端开发从Mapper到Controller的实战记录4.1 用户登录与Token鉴权登录模块是整个系统的入口也是安全的第一道防线。密码绝对不能用明文存储这已经是基本常识了。我的方案是MD5加盐处理在用户创建的时候生成一个随机盐值密码存储为MD5(密码盐值)的结果。登录时用同样的算法计算一遍比对即使数据库泄露了攻击者拿到的也是加盐后的密文想要暴力破解非常困难。当然如果要求更高可以换BCrypt加密但MD5加盐对当前这个项目足够了。登录成功后后端返回一个Token给前端。Token我用的是JWT一种无状态的身份令牌方案。用户登录后拿到Token后续每次请求在请求头里带上后端拦截器校验合法性校验通过就放行。这个流程不会在服务器端存储用户的会话状态对分布式部署和前后端分离架构来说非常友好。JWT本身包含角色信息这样可以从Token里直接解析出当前用户角色不需要每次都查数据库。JWT工具类的核心逻辑大致如下public static String generateToken(Integer userId, String role) { long now System.currentTimeMillis(); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date(now)) .setExpiration(new Date(now EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); }登录接口的实现要注意返回信息的安全不能直接把用户密码字段返回给前端。我在用户实体类上加了JsonIgnore注解密码字段序列化时会自动忽略这样即使不小心把整个用户对象返回密码也不会泄露。4.2 老人管理模块开发全流程老人管理模块是典型的前后端交互流程。前端页面上点击新增老人弹出表单填入姓名、身份证号、性别、出生日期、联系电话等信息点击提交前端数据以JSON格式发送到后端后端通过Controller接收校验参数调用Service层完成业务操作最后把操作结果返回。后端Controller层的写法有一个比较推荐的模式RestController RequestMapping(/api/elderly) public class ElderlyController { Autowired private ElderlyService elderlyService; GetMapping(/page) public Result page(RequestParam Integer pageNum, RequestParam Integer pageSize, RequestParam(required false) String name) { return Result.success(elderlyService.queryPage(pageNum, pageSize, name)); } PostMapping public Result add(RequestBody Elderly elderly) { elderlyService.addElderly(elderly); return Result.success(null); } PutMapping(/{id}) public Result update(PathVariable Integer id, RequestBody Elderly elderly) { elderly.setId(id); elderlyService.updateElderly(elderly); return Result.success(null); } DeleteMapping(/{id}) public Result delete(PathVariable Integer id) { elderlyService.deleteElderly(id); return Result.success(null); } }所有接口统一返回Result对象这个对象包含三个字段code、message、data。code为200表示成功code为500表示失败。这个统一返回格式很重要前端可以基于这个格式封装统一的请求处理和错误提示不用每个接口单独写一遍状态判断。分页查询使用MyBatis-Plus提供的Page对象处理起来非常方便。查询条件用了LambdaQueryWrapper可以根据传入的姓名参数动态拼接条件。模糊查询用like方法实现MyBatis-Plus会自动在SQL层面处理不用自己拼SQL字符串也不会有SQL注入的问题。4.3 多表关联查询与XML写法项目里有一些复杂的查询必须写多表联查。比如查询老人列表的时候要同时把房间号带出来。老人表和房间表通过入住记录表关联这个查询用MyBatis-Plus的QueryWrapper不太好写我就直接在XML里写SQL。select idselectElderlyWithRoom resultTypecom.sunset.entity.dto.ElderlyRoomDTO SELECT e.id, e.name, e.gender, e.id_card, r.building, r.room_no, r.status AS room_status FROM elderly e LEFT JOIN check_in ci ON e.id ci.elderly_id AND ci.leave_time IS NULL LEFT JOIN room r ON ci.room_id r.id WHERE e.deleted 0 if testname ! null and name ! AND e.name LIKE CONCAT(%, #{name}, %) /if ORDER BY e.create_time DESC /select这个SQL用了两个LEFT JOIN。需要注意第LEFT JOIN的关联状态我加了一个AND ci.leave_time IS NULL的条件表示只关联当前有效的入住记录。这是我非常强调的一个细节如果不加这个条件同一个老人退住后再入住就会关联出多条记录查询结果就会出现重复数据。这种多表关联的业务逻辑只有在写真实项目的时候才会暴露出来这也是为什么我建议多看完整项目源码而不是只看简单demo的原因。4.4 全局异常与跨域处理后端接口数量多了以后异常处理如果不统一前端收到的报错信息就会五花八门。我实现了一个全局异常处理器用RestControllerAdvice注解标注统一捕获异常。业务异常返回业务错误码和提示信息系统异常统一返回服务器繁忙的通用提示同时把异常堆栈打印到后端日志方便排查。跨域问题也是前后端分离项目必须面对的。前端服务运行在8081端口后端运行在8080端口浏览器同源策略会拦截跨域请求。解决方式有两种一种是后端配置CORS另一种是前端使用Nginx反向代理。开发环境我选择了后端配置CORS因为不需要启动额外的代理服务测试起来更方便。生产环境用Nginx把前后端配置在同一个域名下从根本上规避跨域问题。两套方案配合使用是我实测下来最稳妥的组合。后端CORS配置比较简单Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }5. 前端开发VueElement UI页面实战5.1 项目创建与目录初始化前端开发的第一步是用Vue CLI创建项目。命令很直接vue create sunset-frontend创建的时候选择Manually select features勾选Router、Vuex、CSS Pre-processors。这里要提醒一句不要为了省事全部默认否则后面装依赖的时候缺一堆东西更麻烦。项目创建好之后先把目录里不需要的HelloWorld组件删掉按照之前规划好的目录结构把views下的各个页面文件夹创建出来。Element UI的引入是在main.js里import Vue from vue import App from ./App.vue import router from ./router import store from ./store import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css Vue.use(ElementUI) Vue.config.productionTip false new Vue({ router, store, render: h h(App) }).$mount(#app)5.2 Axios请求封装与拦截器Axios封装是前端项目里很有必要的一步。我把所有请求集中到一个request.js里统一配置baseURL和请求拦截器。请求拦截器负责在每次发请求前从localStorage中取出Token放到请求头的Authorization字段里。响应拦截器负责统一处理返回结果如果code不是200就弹出一个错误提示如果后端返回401表示Token过期或无效自动跳转到登录页。import axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(登录已过期)) } return res }, error { Message.error(error.message || 请求失败) return Promise.reject(error) } ) export default request5.3 老人管理页面完整实现老人管理页面是典型的管理系统列表页。模板部分用el-table展示列表el-form做搜索区域el-dialog做新增和编辑表单el-pagination做分页。这套模板在很多管理系统里都是通用的做熟之后其他模块的页面基本就是复制改改。老人列表页逻辑如下export default { data() { return { loading: false, tableData: [], pageNum: 1, pageSize: 10, total: 0, searchForm: { name: }, dialogVisible: false, form: { name: , gender: , birthDate: , idCard: , phone: , emergencyContact: } } }, created() { this.loadData() }, methods: { loadData() { this.loading true getElderlyPage({ pageNum: this.pageNum, pageSize: this.pageSize, name: this.searchForm.name }).then(res { this.tableData res.data.records this.total res.data.total }).finally(() { this.loading false }) }, handleSearch() { this.pageNum 1 this.loadData() }, handleAdd() { this.dialogVisible true }, handleEdit(row) { this.form { ...row } this.dialogVisible true }, handleDelete(row) { this.$confirm(确定删除该老人档案吗, 提示, { type: warning }) .then(() { deleteElderly(row.id).then(() { this.$message.success(删除成功) this.loadData() }) }) } } }删除操作一定要加二次确认这是管理系统里的基本交互规范。老人的档案涉及健康记录和费用记录如果误删了数据恢复的成本很高。我在后端删除操作上也做了处理不是物理删除而是逻辑删除数据库中deleted字段置为1。这样做的好处是数据不会真正丢失后续如果需要审计或追溯还能找回。5.4 路由守卫与动态菜单路由守卫是前端登录状态控制的关键。用户未登录时就访问业务页面必须拦截并跳转到登录页。登录成功后根据用户角色生成菜单权限。我用了Vue Router的beforeEach全局前置守卫来实现。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() return } if (!token) { next(/login) return } next() })这个方案简单直接。如果要做动态权限菜单还需要结合后端菜单表根据登录用户的角色过滤路由。在这个项目里我采用了一种折中方案路由全部注册但侧边栏菜单根据角色过滤显示。家属登录时只看到和自己老人相关的菜单看不到费用管理这类后台菜单。虽然前端隐藏了菜单但后端在接口层面同样做了权限校验双重保险前端隐藏只是体验优化后端的权限校验才是真正的安全防线。6. 部署上线全流程从本地到服务器的完整步骤6.1 后端打包与启动项目开发完成后的下一步就是把项目部署到服务器上让别的设备通过网络访问。后端使用Maven打包进入项目根目录执行mvn clean package -DskipTests执行完成后target目录下会生成一个sunset-apartment.jar文件。在服务器上启动这个jar包的时候我用的是nohup java -jar sunset-apartment.jar --spring.profiles.activeprod app.log 21 使用nohup命令让进程在后台运行即使关闭了SSH连接项目也不会退出。日志重定向到app.log文件里方便随时排查问题。启动完成后可以用ps -ef | grep sunset命令查看进程是否正常运行或者直接查看app.log日志里的启动信息。6.2 前端打包与环境配置前端打包之前需要先修改环境配置。开发环境我请求的baseURL是/api打包后我们希望这个请求由Nginx转发到后端服务。所以在打包前确认配置文件中没有写死localhost的地址统一使用相对路径/api开头。执行打包命令npm run build打包完成后项目目录下会生成dist文件夹这个文件夹里就是纯静态文件包含HTML、CSS、JavaScript。直接把这个dist文件夹上传到服务器一般是放在/opt/frontend/dist目录下然后交给Nginx托管。6.3 Nginx反向代理配置Nginx的第一个作用是托管前端静态文件第二个作用是反向代理后端接口。配置如下server { listen 80; server_name your-domain.com; root /opt/frontend/dist; index index.html; location /api/ { proxy_pass http://localhost: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; } location / { try_files $uri $uri/ /index.html; } }这个配置最需要注意的就是proxy_pass后面的斜杠。写的是http://localhost:8080/这就是把/api/路径的请求转发到后端根路径实际请求的路径是http://localhost:8080/elderly/page。如果proxy_pass后面不加斜杠就会把/api/前缀也带到后端后端的Controller就匹配不到了。这个细节曾经让我排查了整整一天真的是血的教训。还要注意location /这一段配置务必配置try_files参数这样前端路由在刷新页面时才不会404。原因很简单Vue是单页应用所有页面实际上都指向同一个index.html如果用户访问某个二级路由时按了一下F5刷新比如访问/elderlyNginx默认会去寻找elderly这个文件找不到就报404。配置了try_files $uri $uri/ /index.htmlNginx找不到具体文件时会自动回退到index.html前端路由接管后就能正确展示对应页面。6.4 服务器部署注意事项首次部署到服务器时有几个细节必须提前检查。服务器的防火墙要放行80和8080端口。云服务商的防火墙和安全组请分别确认有些云服务器需要在安全组里单独配置端口映射不在安全组配置的话即使服务器本地的防火墙是关着的外部流量也进不来。数据库方面远程连接MySQL时需要确认开启了远程访问权限。MySQL默认配置只允许localhost访问需要创建允许远端访问的账号。这里的操作权限较大要谨慎授予最好按最小权限原则只给业务需要的库。CREATE USER sunset_user% IDENTIFIED BY your_password; GRANT SELECT, INSERT, UPDATE, DELETE ON sunset_apartment.* TO sunset_user%; FLUSH PRIVILEGES;7. 常见问题与排查技巧实录7.1 高频问题速查表开发过程中我遇到了不少问题这里总结一张排查对照表都是实际出现过的场景和对应的解决思路现象可能原因解决办法后端启动时报驱动类找不到mysql-connector版本和MySQL 8.0不匹配升级到8.x驱动前端请求接口返回404proxy_pass配置少了斜杠确认nginx配置格式页面刷新后路由404没配置try_files增加try_files $uri $uri/ /index.html查询到的日期少8小时时区设置不正确修改为serverTimezoneAsia/Shanghai中文插入数据库乱码表字符集不是utf8mb4修改表字符集和连接URL上传图片后无法显示静态资源路径配置不对配置静态资源映射或上传到指定目录调用接口提示跨域后端没配置CORS增加CorsConfig配置类数据库连接被拒绝远程访问权限没开启创建远程访问账号身份证校验失败格式校验逻辑有遗漏使用正则表达式全面校验前端打包体积过大引入了不必要的组件使用按需引入Element UI组件7.2 三个高频问题的排查过程第一个是跨域问题。开发环境下前端Vue服务跑在8081端口后端SpringBoot跑在8080端口浏览器直接请求就会触发同源策略拦截。我当时排查这个问题的思路是先打开浏览器开发者工具看请求是否发出再去看后端是否收到了请求。如果请求压根没送达后端就是网络层被拦截检查CORS配置如果请求到达了后端但响应异常就是后端处理逻辑的问题。这个排查思路很重要可以帮快速定位问题在哪个环节。第二个是数据库连接失败。当时报错信息提示Access denied for user我第一反应是账号密码错了检查了半天对不上。后来发现是MySQL 8.0默认加密规则的问题老版本的连接驱动不支持新的加密方式。把mysql-connector-java升级到8.0版本后问题就消失了。这个坑在高版本MySQL下非常典型如果遇到连接报错先检查驱动版本再去考虑账号权限问题。第三个是前端页面白屏。这种情况经常发生在部署上线后本地跑得好好的传到服务器上什么都访问不了。排查发现是Nginx的try_files配置缺失刷新子路由时返回404前端路由没有走到index.html。这个问题很隐蔽因为从页面入口进入一切正常只有从刷新或者直接输入URL访问二级路由时才暴露。7.3 提升开发效率的小技巧开发过程越到后面一些不动声色的细节越是重要。比如后端开发时开启SQL日志打印MyBatis控制台里直接输出SQL语句和参数能快速定位SQL语法问题和数据查询异常。SpringBoot的DevTools热部署插件也要配置好改了代码不用手动重启后端自动刷新每个改动都等重启再运行的话一天时间浪费得太多。前端的开发体验也一样Axios请求可以加一个统一的loading状态管理请求发送时自动开启loading结束后自动关闭省去在每个页面里手动控制。这个逻辑写在一个拦截器里就能生效对开发体验的提升非常明显。另外一点联调阶段必须保持前后端接口文档实时同步。我用的方式比较简单粗暴在项目根目录维护一个API.md文件每次接口变更就顺手更新。虽然云文档更方便但本地文件跟随代码仓库一起维护版本可控接口变更记录都在git历史里能查到。8. 项目复盘与扩展建议项目交付后我重新审视了整个设计与实现过程有几个环节其实是可以做得更好的。比如消息推送当前健康管理模块只做到了记录展示如果老人血压指标异常系统如果能主动给家属推送提醒体验会更好。技术上可以用WebSocket配合定时任务实现后端扫描健康记录表发现异常数据就推送通知给对应老人的绑定家属。这个功能在商业化的养老管理软件中是非常核心的卖点。还有数据报表目前首页仪表盘只是简单的统计卡片。如果要用ECharts做图表分析可以展示各楼栋入住率、月度费用趋势、老人年龄段分布、健康指标走势等内容。对管理决策来说数据可视化呈现的意义非常大。我在做二期迭代的时候计划加入一个完整的统计报表模块基于已经设计好的表结构查询统计并不复杂ECharts有现成的组件可以直接用。另外权限控制还可以进一步细化比如菜单权限和数据权限分离。目前角色权限主要控制在菜单和接口级别数据权限还没有细分。如果想让系统更适合大型公寓运营还需要增加数据权限规则比如护工只能查看自己负责的老人数据不能查看整个公寓所有老人的数据。这个用MyBatis-Plus的DataPermissionInterceptor拦截器可以实现。关于代码的工程质量我觉得有一点值得强调代码注释要养成习惯。不是那种每行都注释的啰嗦写法而是要在关键难点和业务逻辑处给出说明。因为项目是前后端分离的后端的返回结果和状态码含义前端的全局配置和数据格式约定这些在关键位置注明清楚能够节省不少沟通成本。还有一个很多同学忽略的细节数据库脚本的管理。这个项目的SQL脚本我是放在项目根目录的sql文件夹里文件名为sunset_apartment.sql。后续表结构变更我追加变更脚本而不是在原来的脚本上改动每条SQL都有时间戳和变更说明。这样做的好处是任何环境都能从一份基础脚本开始再按顺序执行增量脚本数据结构和版本管理非常清晰。这个习惯是我收到的技术债教训最早做项目的时候直接在线上库改表结构没有备份也没有脚本后来要恢复数据的时候真的欲哭无泪。我在实际开发这个系统的过程中最大的体会是前后端分离的项目难点从来不在某一门技术而在于把一堆技术组合起来解决真实的业务问题。SpringBoot用得再熟练如果不理解数据库表之间的关联关系做出来的模块也只是孤立的CRUD无法支撑完整的业务链路。这就是为什么很多人看教程都会做项目就废的原因。这个夕阳红公寓管理系统算是我做过的项目中比较完整的一个。从需求分析到数据库设计从后端接口到前端页面从本地联调到服务器部署每个环节都走了一遍。虽然和大型商业级系统还有很大差距但作为初学者或者进阶者的练手项目该有的技术点基本都覆盖到了。如果你正在准备毕业设计或者想通过实战项目积累经验可以参考这个项目的技术和设计思路把需求替换成自己熟悉的业务场景然后从头到尾完成整套开发流程收获绝对比看一百篇教程来得多。最后再分享一个部署时的小经验服务器上跑项目JVM内存参数建议显式指认一下。比如我的服务器只有2G内存数据库加后端应用都在上面不指定参数的话JVM可能默认吃掉很大一部分内存系统资源分配会变得很紧张。用java -Xms256m -Xmx512m -jar sunset.apartment.jar启动给后端应用一个合理的上限数据库才能拿到足够的资源。这个细节看似不起眼但在低配服务器上能直接影响项目的稳定性。