ARTICLE DETAIL

资讯详情

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

Spring Boot + Vue3社区养老服务平台开发实战与踩坑复盘

Spring Boot + Vue3社区养老服务平台开发实战与踩坑复盘 做这个项目的时候我前后磨了大概三周时间。从空页面到跑通第一单“上门助浴”服务预约中间踩得最多的不是技术难点而是那些看起来不起眼的小坑。今天就把整个Spring Boot Vue3社区养老服务平台的完整实现过程拆开聊一遍包括为什么选这套技术栈、后端怎么设计核心接口、前端怎么处理多角色权限和可视化大屏以及联调部署阶段遇到的那些真实问题。这不是教科书式的项目介绍是我自己动手写完整个项目后的复盘适合正在做毕业设计、个人项目或者想快速了解前后端分离开发完整流程的朋友参考。1. 项目整体设计与技术选型1.1 养老服务平台解决的核心问题先想清楚一个问题社区养老服务平台到底在解决什么不是把老人信息录进数据库那么简单。我在需求调研阶段跟社区工作人员聊过他们最头疼的是三件事服务工单靠纸质记录容易丢失、老人健康数据分散在多个Excel里没法统一、家属想了解老人情况只能打电话问。所以平台的核心价值应该围绕“服务可追踪、健康可记录、家属可连接”这三个目标来展开。在功能层面我把整个系统拆成了几个核心模块服务预约与工单管理家属或老人线上下单服务人员接单管理员全程追踪工单状态健康档案管理录入老人的基础健康指标血压、血糖、心率等支持周期性体检数据导入活动公告与报名社区活动发布、老人或家属在线报名、签到统计紧急呼叫与异常预警一键呼叫功能后台实时推送异常情况多角色权限老人/家属、服务人员、社区管理员三类角色的数据隔离与功能隔离1.2 为什么选择Spring Boot Vue3这套组合这个选型不是拍脑袋决定的我对比过几种方案。第一版原型其实用的是JSP Servlet那套老技术做完登录页面我就放弃了。原因很实际JSP页面服务端渲染虽然简单直接但前后端耦合严重后期加一个页面改动就要重启整个Tomcat而且现代前端组件库基本没法用。后来换成前后端分离架构后端用Spring Boot前端用Vue3开发效率提升非常明显。后端选Spring Boot的理由比较朴素Java生态在中小型管理系统的业务表达上非常成熟稳定。Spring Boot的自动配置和约定优于配置设计让项目初始化成本很低官方文档和社区资料也足够丰富。尤其像Spring Security、MyBatis-Plus这些配套组件能让权限控制和数据库操作少写大量样板代码。对于这种带多角色权限、复杂业务状态流转的管理系统Spring Boot的工程化优势非常明显。前端选Vue3则是看中了它的组合式API对复杂交互逻辑的整理能力。对比Vue2的选项式APIVue3的setup语法糖在组件复用和逻辑抽离上更灵活。比如我要在服务工单列表和健康档案两个完全不同的页面里使用同一套分页逻辑用composables直接抽一个usePagination函数出去两边引用就行。而且Vite的热更新速度比Webpack快很多项目大了以后体感非常明显。1.3 整体架构设计与数据库规划系统的整体架构是标准的前后端分离模式vue3前端Vite构建 │ 通过HTTP/JSON交互携带JWT Token ▼ Spring Boot后端RESTful API │ ├── 认证模块JWT Spring Security ├── 业务模块用户、服务工单、健康档案、活动、公告 └── 数据层MyBatis-Plus MySQL 8.0这种架构最直接的好处是前后端可以并行开发。我这边后端接口还没写完前端同事或者说另一个终端已经可以用Mock数据先把页面做出来了。只要提前约定好接口返回格式{ code, data, message }联调阶段基本不会出现大冲突。数据库设计上我比较关注表之间的关联关系。核心表包括user平台登录账号通过role字段区分老人/家属/服务人员/管理员elder_info老人详细信息与user是一对一关系service_order服务工单表核心的业务流转表health_record健康指标记录表community_activity活动发布表activity_signup活动报名表设计这些表的时候最容易犯的错误是把字段铺得太大。比如第一个版本里我为了“省事”在service_order表里直接存了服务人员的名字和电话后来发现如果服务人员修改了手机号历史工单里就留下旧号码。正确的做法是关联user_id查询时再JOIN用户表。这一点建议新手特别注意数据冗余不是不能用但一定是在认清了业务场景之后再用。2. 后端核心实现Spring Boot2.1 后端工程结构与初始化项目初始化我推荐用Spring Initializr无论是Idea内置的还是网页版都可以。需要注意选择的依赖版本要和自己本机的JDK版本匹配。我这里用的环境是JDK 17 Spring Boot 2.7.8这个组合相对稳定。选完依赖后生成的工程结构大概是src/main/java/com/example/eldercare/ ├── config/ // 配置类 ├── controller/ // RESTful API入口 ├── service/ // 业务逻辑层 ├── mapper/ // 数据访问层 ├── entity/ // 数据库实体 ├── dto/ // 数据传输对象 ├── common/ // 通用类返回结果、异常处理等 └── utils/ // 工具类包结构的划分对应了清晰的分层职责。Controller层只做参数的接收和校验不写业务逻辑Service层专注业务规则比如创建工单时要联动检查用户余额、服务时间是否冲突等Mapper层是最底层的SQL操作。建议在项目初期就坚持这个规范不然后期维护会让你崩溃。我在pom.xml里主要引入了这些关键依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependencyMyBatis-Plus在这里分担了很大工作量。单表CRUD基本不需要写SQLBaseMapper里的selectById、selectPage已经够用。复杂一点的多表关联查询才需要自己写XML或注解SQL。2.2 用户认证与权限控制用户认证这块我走了两条路线开始想用Spring Security里的JWT过滤器自己写后来发现配置链路太长就换成了拦截器方案。虽然在Spring Boot项目里面自己写拦截器确实不如Spring Security严谨但对这种规模的项目来说简单的JWT校验已经能满足需求。具体实现是在config包下定义一个JwtInterceptor在addInterceptors里注册同时配置不需要拦截的路径白名单登录、注册、验证码等。每次请求进来会先检查Header里的Authorization字段取到等号前的“Bearer ”前缀解析出用户ID和角色然后放在ThreadLocal或RequestAttributes中供后续代码使用。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String authHeader request.getHeader(Authorization); if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(Bearer )) { throw new BusinessException(401, 未登录或登录已过期); } String token authHeader.substring(7); Claims claims JwtUtil.parseToken(token); if (claims null) { throw new BusinessException(401, 登录状态无效请重新登录); } request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }这里有两个地方容易踩坑。第一个是JWT密钥长度jjwt要求签名密钥不能太短我一开始用了一个很短的字符串一直报WeakKeyException后来改成256位以上的随机字符串才正常。第二个是过期时间按需求可以设为24小时或7天但一定记得在新增角色权限控制时读取Token里的过期时间否则后台改完权限要等很久才能生效。2.3 养老服务的核心业务服务工单闭环服务工单是整个平台业务的中枢从创建到完整体现了业务闭环。我把工单状态设计成了五态待接单 → 已接单 → 服务中 → 已完成 → 已取消。状态流转不是随便改字段值每个状态变更都要满足前置条件比如“已完成”必须有服务人员和老人的签到记录“已取消”必须在服务开始前才有权限操作。Service public class ServiceOrderService { Transactional(rollbackFor Exception.class) public Long createOrder(ServiceOrderCreateDTO dto, Long userId) { ServiceOrder order new ServiceOrder(); order.setElderId(dto.getElderId()); order.setServiceType(dto.getServiceType()); order.setAppointmentTime(dto.getAppointmentTime()); order.setAddress(dto.getAddress()); order.setStatus(OrderStatusEnum.PENDING_ACCEPT.getCode()); order.setCreateBy(userId); // 校验同时间段内是否存在已接受的冲突工单 long conflictCount orderMapper.checkConflict(dto.getElderId(), dto.getAppointmentTime()); if (conflictCount 0) { throw new BusinessException(400, 该时间段已存在服务工单请选择其他时间); } orderMapper.insert(order); return order.getId(); } }创建工单时有一个业务细节需要校验同一老人同一时间段是否已有未完成的工单。这个校验如果用代码逐条比对可能丢事务我给service_order表加了一个组合索引(elder_id, appointment_time)查询时直接用索引过滤。这套设计上线后基本没有出现过重复服务的投诉。状态更新建议用状态机的方式为了简单我用了一个MapOrderStatusEnum, ListOrderStatusEnum来定义合法流转路径。后续如果要增加“重新派单”或“超时自动取消”的状态只需要集中修改这张映射表不用到处找判断逻辑。2.4 健康档案与数据预警健康档案模块本身不复杂就是老人的身高、体重、血压等指标的增删改查。难点在于这些数据的分析展示需要支持趋势图。后端接口出数据的时候我不建议让前端自己算周平均值直接在SQL层用聚合函数把一周或一个月的趋势值算好返回前端只负责渲染。Select(SELECT DATE_FORMAT(record_date, %Y-%m-%d) AS recordDate, AVG(systolic_pressure) AS avgSystolic, AVG(diastolic_pressure) AS avgDiastolic FROM health_record WHERE elder_id #{elderId} AND record_date #{startDate} GROUP BY DATE_FORMAT(record_date, %Y-%m-%d) ORDER BY record_date ASC) ListHealthTrendVO selectWeekTrend(Param(elderId) Long elderId, Param(startDate) String startDate);做健康预警时还有一个细节容易被忽略连续性判断。比如老人今天血压偏高不一定是异常但如果连续三天都偏高就要触发预警通知家属。这部分判断我放在了定时任务里每晚会跑一个Scheduled方法扫描最近三天的健康数据如果发现异常就新增预警记录并推送通知。定时任务在这个项目中承担了类似后台“哨兵”的角色。3. 前端核心实现Vue3 Vite3.1 使用Vite创建Vue3项目与目录规划创建前端项目我直接用了Vite。对比WebpackVite的启动速度真的快太多了。npm create vitelatest elder-care-frontend -- --template vue cd elder-care-frontend npm install装完后需要自己补齐项目运行时依赖。我用到了vue-router路由、pinia状态管理、element-plusUI组件库、axios网络请求、echarts可视化图表。目录结构也做了模块化划分src/ ├── api/ // 接口请求封装 ├── assets/ // 静态资源 ├── components/ // 公共组件 ├── composables/ // 组合式函数 ├── layout/ // 页面框架 ├── router/ // 路由配置 ├── store/ // Pinia状态 ├── utils/ // 工具函数 ├── views/ // 页面组件 │ ├── dashboard/ // 可视化大屏 │ ├── order/ // 工单管理 │ ├── health/ // 健康档案 │ └── system/ // 系统管理 └── App.vue在项目初始化时有一个特别容易踩的坑npm install之后直接跑npm run dev有时候会提示vite版本和vitejs/plugin-vue版本不匹配导致页面启动空白。这类问题大概率是Node版本过旧。如果遇到了建议先升级到Node 16.18以上然后删除node_modules和package-lock.json重新安装。3.2 登录态管理与Axios拦截器前端的登录态管理是整个权限控制体系的前端配合部分。用户登录后后端会返回JWT Token和用户基本信息我把Token存到Pinia的持久化存储里并统一封装Axios请求。// src/utils/request.js import axios from axios import { useUserStore } from ../store/user import { ElMessage } from element-plus const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 15000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { if (res.code 401) { useUserStore().logout() router.push(/login) } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )这里特别强调baseURL的配置。开发时我习惯用/api前缀配合Vite的代理配置把请求转发到后端。这样线上部署时只需要改Nginx的代理不用动业务代码。3.3 多角色动态路由与路由守卫这个平台有三类核心角色不同角色看到的菜单和页面完全不同。管理员看工单管理、用户管理、活动管理服务人员看自己的接单列表和个人中心老人/家属看服务预约、健康档案和活动报名。如果全部都靠v-if控制组件渲染代码会非常凌乱。我的方案是让后端在登录接口中返回用户角色和可访问的路由标识前端根据路由标识动态注册路由。// src/router/index.js const constantRoutes [ { path: /login, component: () import(../views/login/index.vue) }, { path: /, component: () import(../layout/index.vue), redirect: /dashboard, children: [] } ] const asyncRouteMap { admin: [ { path: /order, name: OrderList, component: () import(../views/order/list.vue) }, { path: /user, name: UserList, component: () import(../views/user/list.vue) } ], worker: [ { path: /order/my, name: MyOrders, component: () import(../views/order/my.vue) } ], family: [ { path: /service, name: ServiceReserve, component: () import(../views/service/reserve.vue) }, { path: /health, name: HealthRecord, component: () import(../views/health/index.vue) } ] }在路由守卫里判断当前用户角色首次进入系统时动态添加路由。这样不仅能实现菜单的按角色展示还能在用户通过URL直接访问无权限页面时自动拦截跳转。3.4 可视化大屏与数据看板平台首页我设计了一个可视化大屏展示今日服务订单量、服务完成率、健康异常预警数、月度服务趋势等核心指标。这一块用ECharts实现是最常见的方案但中间有一些细节值得注意。大屏页面需要自适应屏幕尺寸组件用resize事件监听变化。script setup import * as echarts from echarts import { ref, onMounted, onBeforeUnmount } from vue const chartRef ref(null) let chartInstance null const renderChart () { if (!chartRef.value) return chartInstance echarts.init(chartRef.value) chartInstance.setOption({ // 服务趋势、分类占比等配置 }) } const handleResize () { chartInstance chartInstance.resize() } onMounted(() { renderChart() window.addEventListener(resize, handleResize) }) onBeforeUnmount(() { window.removeEventListener(resize, handleResize) chartInstance chartInstance.dispose() }) /script看着很简单但有几个容易踩的坑。ECharts数据异步加载时setOption会重复合并建议在拿到接口返回数据后再创建实例图表容器如果初始隐藏Vue的v-show为false绘图时容器宽度是0图表会挤成一条竖线。我的经验是先把容器显示出来或者用nextTick确保DOM渲染完毕再初始化。4. 前后端联调与打包部署4.1 跨域问题与开发环境代理前后端联调时必遇到的一个问题前端跑在5173端口后端跑在8080端口直接请求会被CORS拦截。我解决这个问题没有选择在后端加CrossOrigin注解而是在前端Vite配置了代理。// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这样前端请求/api/service/order时实际会被转发到http://localhost:8080/service/order。最关键的是这个代理配置不用改代码开发和生产环境都能用后端也不需要去处理烦人的CORS预检请求。有一点提醒跨域问题如果直接在后端开启CrossOrigin(*)对生产环境是有安全风险的。我见过几个同学的项目为了省事全局开启了跨域上线后造成接口被跨站请求调用。代理方式更安全。4.2 打包发布后端和前端后端打包用Maven打包成Jar包mvn clean package -DskipTests java -jar target/eldercare-server.jar前端打包需要先配置环境变量。我在项目根目录建了.env.production文件VITE_API_BASE_URL/prod-api然后执行npm run build产物会生成在dist目录。部署时我用Nginx托管这个目录并配置反向代理转发到后端服务。server { listen 80; server_name elder.example.com; location / { root /var/www/eldercare; index index.html; try_files $uri $uri/ /index.html; } location /prod-api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里最核心的是try_files $uri $uri/ /index.html。Vue3是单页应用路由切换是靠前端Router控制的服务端如果找不到路径直接返回404刷新页面就会出现白屏。我部署的时候第一次忘了配这行刷新详情页直接404排查了半个小时才反应过来。5. 常见问题与排查技巧实录5.1 启动Spring Boot不显示端口号这个问题在搜索热词里出现了很多人启动Spring Boot项目只看到Spring Logo但控制台没有Tomcat started on port(s): 8080。最常见的原因是项目里同时引入了spring-boot-starter-web和某个测试框架或者启动类放错了位置没有扫描到Controller。排查思路先看启动类确认它在包的根路径比如com.example.eldercare然后看看application.yml是否显式配置了server.port但被其他profile覆盖。如果都没问题就在启动类上加ComponentScan明确指定扫描路径。还有一个容易忽略的原因Java进程其实已经启动过了控制台把旧日志清掉了。可以试试lsof -i:8080看端口是否被占用。5.2 上传文件报413错误项目里允许上传老人的体检报告或活动照片。通过Nginx部署后上传大文件报413 Request Entity Too Large。这个是Nginx配置问题不是后端代码Bug。需要修改Nginx的client_max_body_size参数location /prod-api/ { proxy_pass http://127.0.0.1:8080/; client_max_body_size 50m; }同时后端也要配合调整Spring Boot的文件大小限制。Spring Boot 2.x默认单文件最大1MB批量文件10MB需要在application.yml里修改spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB5.3 Vue3中ECharts在rem适配下缩放异常项目用了postcss-pxtorem做移动端适配结果发现ECharts图表里的字体、边距完全不随屏幕缩放。原因是ECharts的canvas画图是在JS里用像素值控制的pxtorem只能转换CSS样式管不到JavaScript的数值型配置。解决思路有两种一是监听窗口变化手动调用chart.resize()并重新计算配置项里的字号二是用echarts.init的renderer参数配合devicePixelRatio在移动端把大屏适配方案改成用scale缩放整体图表容器而不是依赖CSS单位。我采用的是第一种虽然稍微麻烦但效果最可控。5.4 Vue2转Vue3需要改的注意点如果你是从Vue2迁移过来的最容易踩的坑集中在3个地方。第一生命周期。beforeDestroy改成了beforeUnmountdestroyed改成了unmounted。第二全局API的使用方式变了。Vue2的Vue.prototype.$http和Vue.use在Vue3里要改成app.config.globalProperties和app.use。第三过滤器filter被移除了只能用计算属性或方法替代。我迁移的时候在v-for里用了一个时间格式化过滤器改完编译不通过才查文档发现Vue3已经不支持了。5.5 其他值得收藏的小经验再补充几条实践中的经验。MyBatis-Plus的字段自动填充要配置好MetaObjectHandler否则create_time、update_time这种字段每次插入都是空值。JWT过期时间和Redis缓存过期时间要设置成一致不然用户明明刷新了Token却还是被登出。还有一个关于前端的问题Vue3的v-model语法糖在组件通信时是update:modelValue事件不是Vue2的input事件自定义弹窗组件时很容易搞混。写在最后的实操感受整个项目开发下来回到开头那句话真正的技术瓶颈其实不多难的是把这些技术组件组合起来真正跑通一个业务闭环。Spring Boot约定优于配置和Vue3的组合式API在开发效率上给我的体验是11大于2的——后端只专注暴露数据接口前端把交互体验和界面表现做好定位清晰很多。我最想提醒后来者的一点是不要沉迷于给项目加各种新框架和技术栈。社区养老服务平台的核心价值在于工单闭合的业务流、健康数据的稳定采集以及能让社区工作人员真正愿意使用。如果你正在做类似的项目先把“一个老人成功预约一次上门服务服务人员准时上门完成后家属能查到回执”这条主链路跑通再考虑加Redis缓存、消息队列这些增强功能。工具永远是手段把老人们的服务体验照顾好才是所有技术投入的意义。最后再分享一个小细节我给所有核心接口都加了请求日志用Slf4j记录入参和出参后来排查问题的时候省了太多事。开发阶段觉得多余的代码往往是上线后救命的东西。
返回列表