
1. 项目整体设计为什么是SpringBootVue而不是别的1.1 先搞清楚养老智慧服务平台到底在管什么很多人第一次听到“养老智慧服务平台”脑子里会跳出一堆概念智能手环、人脸识别门禁、呼叫中心、大屏指挥……这些确实是智慧养老的组成部分但如果只盯着硬件噱头去做系统最后往往会变成一个“看得到但用不起来”的演示项目。我拿到这个标题的第一反应是先划边界。SpringBootVueMySQLMyBatis这个组合本质上交付的是一套业务管理系统核心场景不是物联网设备接入而是围绕“老人—家属—护工—平台管理员”这几类角色把日常养老服务跑通建档、评估、预约、派单、执行、回访、计费、健康数据沉淀。也就是先有服务流程的数字化再谈智能硬件和大屏可视化。和普通商城类管理系统相比养老平台有几个明显特点服务对象特殊。老人大多不会自己操作App实际下单和查看记录的人往往是家属、社区工作者或护工所以权限体系和操作流程都要按代运营思路设计。服务过程有强状态流转。一次助浴服务从家属下单到护工接单、上门、签到、完成、家属确认、平台回访每一步都要留痕不能像电商订单那样简单“发货—收货”就结束。健康数据敏感。血压、血糖、用药记录这些内容一旦录入错误可能直接影响后续决策所以表单校验、修改留痕、数据备份都要比普通业务系统严格一点。管理端、服务端、移动端诉求差异大。虽然源码里可能只有PC管理端但设计时要留好API扩展空间以后接小程序或App才不会伤筋动骨。想清楚这几点再去看技术选型就顺理成章了SpringBoot负责稳定输出REST接口Vue负责把后台管理页面做得清晰好用MySQL存业务数据MyBatis写灵活的SQL应对各类统计报表。这个组合不是最新潮的却是最稳妥、最容易维护、也最适合中小型团队接手的一套。1.2 技术选型背后的取舍逻辑以我的经验来说项目标题里把技术栈亮出来往往意味着这本身就是一个面向毕业设计、课程实训或中小型商用项目的方案。这类项目最怕的不是功能少而是过度设计。原因很简单接手的人可能只有Java基础和一点前端经验框架太重、抽象层次太多出了问题很难定位。为什么是SpringBoot而不是SSH或SpringMVC不用多说SpringBoot把自动配置、内嵌Tomcat、起步依赖都做好了一个main方法就能跑起来。关键是它默认的组件扫描和配置体系让MyBatis、MySQL、Redis、定时任务这些常用部件都能以极低的上手成本集成。对于养老这种业务耦合度高、迭代快的系统快速交付比炫技重要得多。为什么是MyBatis而不是JPA养老平台的查询条件很难提前固定。例如“查询60岁以上、有糖尿病史、所在社区为某街道、最近一周有未完成订单的老人”这种条件组合在JPA里写会变成一堆Specification或QueryDSL代码直接维护的人崩溃。MyBatis把SQL写在XML里条件拼接一目了然更重要的是平台后续要做运营统计报表靠MyBatis写复杂聚合SQL远比ORM改造来得直接。代价是需要手写映射但只要你建表规范、字段命名对齐实体类这部分工作量是可控的。为什么是MySQL 5.7/8.0养老平台的并发量不会像电商抢购那么夸张MySQL的稳定性和生态足够覆盖。而且运维招人时MySQL DBA比PostgreSQL好找得多。标题里着重提了JavaMySQLMyBatis完整源码说明这套组合的复制成本低拿到源码往本地一导就能跑这一点对学习型项目尤其重要。为什么是Vue而不是React或Angular国内后台管理系统的团队协作习惯里Vue配套的Element Plus或Ant Design Vue组件库能把表格、表单、弹窗、树形菜单这些后台高频组件开箱即用地组合起来。Vue的模板语法对后端转前端的开发者也更友好路由和状态管理同样轻量。如果目标是快速做出一套整洁的管理界面Vue是目前综合成本最低的选项。1.3 源码工程的合理结构长什么样一个合格的SpringBootVue完整源码至少应该包含三块后端Java工程、前端Vue工程、数据库初始化脚本。我比较推荐构建如下结构pension-platform/ ├── backend/ │ ├── src/main/java/com/example/pension/ │ │ ├── controller/ # 控制层只做参数接收和结果封装 │ │ ├── service/ # 业务层事务边界在这里 │ │ ├── mapper/ # MyBatis Mapper接口 │ │ ├── entity/ # 数据实体 │ │ ├── dto/ # 前后端交互对象 │ │ ├── config/ # 跨域、拦截器、WebMvc配置 │ │ ├── common/ # 统一响应、异常、常量、工具类 │ │ └── PensionApplication.java │ └── src/main/resources/ │ ├── mapper/ # MyBatis XML映射文件 │ └── application.yml ├── frontend/ │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── views/ # 页面视图 │ │ ├── router/ # 路由配置 │ │ ├── store/ # Pinia/Vuex状态 │ │ ├── components/ # 公共组件 │ │ └── utils/ # 请求工具、格式化 │ └── package.json └── sql/ └── pension_db.sql # 建库、建表、初始化数据后端分层的核心原则是Controller不写业务逻辑Service不直接操作HttpServletRequestMapper只负责SQL数据访问实体类不继承乱七八糟的父类。这种“贫血模型”可能被某些人嘲笑说不够DDD但对于这种规模的项目简单分层就是最大的可维护性。前端方面views目录按业务模块分子目录每个模块一个文件夹里面放index.vue和对应的子组件api目录按后端Controller对应拆文件这样前后端接口对应关系能很直观地找出来。数据库脚本是整个项目的地基我见过太多源码里.sql文件缺失或版本不一致导致导入后缺表、缺字段。所以交付完整的pension_db.sql是项目的底线里面除建表语句外还要包含默认管理员账号、基础服务项数据、几组演示用老人档案。这样拿到代码的人第一次启动就能看到数据而不是面对一片空白。2. 核心模块设计与数据库建模实操2.1 老人档案与健康档案建模运营养老平台的第一步不是做界面而是建档案。老人档案表是整个系统的数据中心几乎所有业务表都会直接或间接引用它。这张表设计得好不好直接决定后续查询和统计是否顺手。最基础的表可以这样设计字段名类型说明elder_idbigint主键自增elder_namevarchar(50)老人姓名gendertinyint性别 0-未知 1-男 2-女id_cardvarchar(18)身份证号建议唯一索引birthdaydate出生日期phonevarchar(20)联系电话addressvarchar(200)现居住地址community_idbigint所属社区/街道编号emergency_contactvarchar(50)紧急联系人姓名emergency_phonevarchar(20)紧急联系人电话health_leveltinyint健康等级1-自理 2-半自理 3-失能statustinyint档案状态0-正常 1-已注销create_timedatetime创建时间update_timedatetime更新时间关键点一是id_card要加唯一索引后续登录、查询、对接外部系统都用它做关联重复数据会造成基础数据混乱关键点二是emergency_contact和emergency_phone不能合并到监护人表里因为有些老人没有固定监护人紧急联系人是社区工作人员独立字段能减少表关联复杂度。健康档案表则跟老人形成一对多关系因为老人每次体检、测量、用药调整都会生成一条新记录历史数据必须保留。建议拆成两张表elder_health_record存每次建档评估的概要elder_health_detail存具体指标项比如血压高低压、空腹血糖、心率、用药名称和剂量。如果数据量不大也可以把指标字段冗余在一条记录里但字段一定不能是varchar存JSON串否则后来统计血压异常人群时SQL会写得很难受。这里有一个容易被忽略的坑不要把“年龄”作为字段存储因为年龄会随时间变化正确做法是存birthday查询时用TIMESTAMPDIFF(YEAR, birthday, CURDATE())计算。否则每年还得定期跑任务更新年龄字段既不安全也没必要。2.2 服务预约与工单流转养老服务最核心的流程是预约派单。我建议把订单表、工单表、服务项目表分开设计而不是所有内容塞进一张大表。service_item服务项目表项目名、服务时长、单价、服务内容说明、是否上门、状态。service_order订单主表订单编号、老人ID、家属/下单人ID、服务项目ID、预约时间、期望服务时间段、地址、备注、订单金额、状态、创建时间。service_order_worker工单执行表订单ID、护工ID、接单时间、服务开始时间、服务结束时间、完成情况、签到照片URL、老人/家属评价、评价内容。订单状态机是这类系统的灵魂。推荐的状态列表为待接单、已接单、服务中、待确认、已完成、已取消、已退款。每个状态变更都要写时间戳和操作人这样后续无论是计费、纠纷溯源还是运营统计都有数据可查。状态流转的约束应该放在Service层而不是前端判断。例如只有“待接单”状态下的订单才允许护工接单“服务中”不能直接被取消。如果状态散落在各个Controller里后面维护一定出野路子流转。可以采用简单的状态机枚举加Service方法校验不用引入复杂的Flowable工作流引擎现阶段没到那份上。支付相关逻辑如果只是毕设或演示项目就没必要接微信支付直接在订单表里加pay_status字段用“未支付、已支付、已退款”标记即可前端和管理端能展示就够了。真实商用再加支付回调也不晚。2.3 紧急救助与告警模块智慧养老平台另一个核心能力是处理老人紧急求助。这个模块通常包含两部分主动求助老人按SOS按钮和被动告警体征异常、设备离线、长时间无活动。告警记录表可以这样设计alert_id告警主键elder_id老人IDalert_type告警类型1-SOS 2-体征异常 3-设备离线 4-长时间未活动alert_content告警内容描述alert_time告警发生时间handler处理人IDhandle_status处理状态0-未处理 1-处理中 2-已闭环handle_result处理结果说明handle_time处理时间这类表的设计重点是alert_status不能只分“未处理/已处理”必须加一个“处理中”的中间态。实际工作中告警电话打过去没人接、上门查看需要时间过程往往持续几分钟甚至几个小时如果直接标记已处理管理端复盘时就不知道当时干预到哪一步了。前端在管理端会有一个告警列表未处理记录需要高亮显示最好能按社区筛选、按类型筛选。考虑到告警数据的实时性比一般业务数据高前端后台可以做定时轮询比如每30秒刷新一次暂不需要上WebSocket。等日活上去之后再改造为长连接也不迟。2.4 权限管理设计养老平台的角色权限一定要用RBAC模型因为实际运营中角色会变比如同一个工作人员可能既是护工又是社区管理员还可能是某个老人的家属。如果给用户表直接加“角色字段”存多个角色ID后面接口鉴权时还需要解析字符串麻烦且容易出错。推荐的表结构是标准的五张核心表sys_user系统用户字段包括user_id、username、password、real_name、phone、status、create_timesys_role角色表字段包括role_id、role_name、role_code、remarksys_menu菜单/权限表字段包括menu_id、parent_id、menu_name、path、perms、menu_type、icon、order_numsys_user_role用户角色关联表sys_role_menu角色菜单关联表登录后把用户拥有的权限标识集合perms加载到内存写一个自定义注解配合Spring AOP或拦截器做接口鉴权。例如RequirePermission(pension:order:accept)这样代码里每次涉及敏感操作时一个注解就能控制访问范围可读性和安全性都远比赛后在Controller里手动判断“当前用户的roleId是否等于1”要好。MySQL设计上关联表的主键最好是联合主键比如sys_user_role的user_idrole_id避免重复插入。删除用户的接口要同时清理关联表数据防止残留脏数据影响查询。3. 后端关键实现SpringBoot MyBatis MySQL的工程落地3.1 创建SpringBoot工程与pom.xml依赖用IDEA创建SpringBoot工程的时候最怕遇到版本选择困难。我建议SpringBoot 2.7.x不要上来就选3.x。原因很直接3.x基于JDK17很多老教程、老依赖版本兼容性有坑MyBatis的starter在新版本下偶尔会出现映射扫描调整的问题对于养老平台这种追求稳定的项目2.7系列是这几年验证最充分的版本。JDK选1.8即可绝大多数服务器还是这个环境。pom.xml里关键依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependenciesPageHelper这个插件我建议一开始就加上。后台管理系统的每个列表几乎都逃不过分页手写LIMIT参数又容易忘掉处理当前页数和每页条数的边界PageHelper把物理分页的活干了同时还能保持Mapper里的SQL干净。不过注意PageHelper分页一定要在执行SQL前调用PageHelper.startPage()紧接着执行Mapper查询中间不能夹别的方法否则分页数据跟总数对不上。3.2 application.yml配置与多环境开发环境的配置如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pension_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 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 pagehelper: helper-dialect: mysql reasonable: true这里有几个细节值得专门讲一下map-underscore-to-camel-case: true是MyBatis的救星。表字段elder_name可以直接映射到实体类elderName属性不用每个字段都写resultMap。但如果表里有多个单词拼的字段例如create_time实体类叫createTime映射时就能省下大量冗余配置。serverTimezoneAsia/Shanghai一定要配。MySQL 8.x的驱动对时区敏感不配经常会碰到日期时间差8小时或者直接连接报错。字符集编码也别省否则存中文出现乱码时排查成本远高于提前配置。log-impl配置为StdOutImpl让MyBatis在控制台打印SQL开发调试阶段非常有用。可以清楚看到每次请求对应执行的SQL、参数值和影响行数联调时基本不需要再靠猜测。多环境配置我习惯拆成三个文件application.yml公共配置、application-dev.yml开发环境、application-prod.yml生产环境。prod里把SQL日志关掉密码通过环境变量注入避免明文写到配置文件后上传Git仓库发生泄露。如果用maven打包可以加Profile切换打包环境但最简单的方式还是启动参数指定java -jar pension.jar --spring.profiles.activeprod3.3 MyBatis映射文件与动态SQL写法要点项目里最复杂的查询往往出现在管理台列表页。比如订单管理列表需要按订单号模糊搜索、按老人姓名搜索、按服务项目搜索、按状态筛选、按时间范围筛选还得关联出老人姓名、护工姓名、服务项目名称。这种场景正是写动态SQL的典型。select idselectOrderList resultTypecom.example.pension.dto.OrderQueryDTO SELECT so.order_id, so.order_no, e.elder_name, e.phone AS elder_phone, si.item_name, so.appoint_time, so.service_address, so.status, so.order_amount, u.real_name AS worker_name FROM service_order so LEFT JOIN elder_info e ON so.elder_id e.elder_id LEFT JOIN service_item si ON so.item_id si.item_id LEFT JOIN sys_user u ON so.worker_id u.user_id where if testorderNo ! null and orderNo ! AND so.order_no LIKE CONCAT(%, #{orderNo}, %) /if if testelderName ! null and elderName ! AND e.elder_name LIKE CONCAT(%, #{elderName}, %) /if if teststatus ! null AND so.status #{status} /if if teststartTime ! null AND so.create_time gt; #{startTime} /if if testendTime ! null AND so.create_time lt; #{endTime} /if /where ORDER BY so.create_time DESC /select几个要点LEFT JOIN而不是JOIN。老人或护工信息可能在极端情况下缺失LEFT JOIN可以保证订单记录仍然显示不会因为关联表数据被删而查不到。if标签判断空串。MyBatis传参时前端很可能传一个空字符串出来不加null和空串双重判断SQL会莫名带一个空的LIKE条件虽然影响不大但显得不严谨。#{}和${}不能混用。排序字段这种需要动态传入列名的地方可以使用${}但前提是列名必须经过白名单校验。所有用户输入的内容都必须用#{}这是防止SQL注入的底线。 和 。XML里直接写号可能解析报错大于号在XML里是合法的但小于等于会被解析成标签开头所以要用转义字符。查询结果可以封装到DTO而不是直接用Entity因为列表页面需要展示的字段常常跨越三四张表Entity根本承载不了。用独立的OrderQueryDTO字段按页面需求裁剪前端拿到的JSON也会更干净。3.4 统一响应与全局异常处理后端接口如果各返回各的格式有的直接返回Map有的返回一个序列化对象前端axios拦截器就没法做统一处理。从一开始就约定好统一响应体Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }对应地全局异常处理器要捕获三类异常业务异常订单状态不允许操作、参数校验异常前端必填项没传、兜底异常数据库挂了之类的意外。业务异常可以通过自定义BusinessException在Service层主动抛出异常处理器里统一返回code500、message等于自定义提示。参数校验异常通常由Valid触发处理器里把字段错误信息拼装成中文提示返回给前端。有个细节很多教程不会讲Result.error里的message尽量不要直接透出数据库异常原文例如“Duplicate entry xxx for key id_card”这种信息直接给用户看既不友好也可能泄露表结构。应该捕获唯一约束冲突后转化成“该身份证号已存在档案”。我在全局异常处理里专门对SQLIntegrityConstraintViolationException做了一次包装实测下来前端提示体验提升非常明显。3.5 登录认证与JWT养老平台的登录认证我建议用JWT而不是Session。原因是前后端分离后后端接口会被PC管理端、移动端、小程序共用Session在跨域和App场景下都不好维护而JWT无状态只要前端每次请求把token放Header里带着就行。实现思路很简单登录接口接收username和password校验通过后用JWT生成token把用户ID和用户名编码进去设置过期时间建议2小时。写一个拦截器或过滤器对所有除白名单外的接口做token解析。解析失败返回401前端收到401后跳回登录页。从token解析出userId后放到ThreadLocal或Request Attribute里Service层需要当前登录人时直接获取。public class JwtUtils { private static final String SECRET your-secret-key; private static final long EXPIRE 2 * 60 * 60 * 1000L; public static String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }密码存库绝对不能是明文。用Spring Security的BCryptPasswordEncoder或者直接引入jBCrypt库存hash值。登录验证时只比对hash即使数据库泄露黑客也不能直接拿密码去撞其它平台。这个点算是最基础的安全底线见过不少“完整源码”里密码明文存储这种项目被脱库一次就是安全事故。安全方面还有两个容易被忽略的小点。第一登录接口要做失败次数限制连续失败5次后锁定账号10分钟否则老龄服务平台的护工账号被暴力破解后续订单、健康数据都会面临泄露风险。第二运营人员操作健康档案的接口要记录操作日志至少留下操作人、操作时间、修改前后的关键字段方便出问题时追溯。4. 前端Vue实现页面到接口联调4.1 Vue项目环境与基础结构用Vite创建Vue项目是目前的主流比Vue CLI的启动速度快得多。创建命令很简单npm create vuelatest pension-admin创建时按需选择Vue Router、Pinia、ESLint都可以选上TypeScript如果团队不熟悉就先选No避免额外的类型调试成本。实际开发中Element Plus是管理端最顺手的组件库注册方式npm install element-plus在main.js里全量引入即可。后台管理系统页面数量不算太大全量引入不会造成明显的性能问题不必刻意做按需自动导入省下来的配置时间更值钱。路由设计采用静态路由加动态权限的方案。基础路由有登录页、404页、首页。登录成功后前端根据后端返回的菜单权限列表动态调用router.addRoute添加业务页面路由。这里有个常见的坑刷新页面后Pinia里的菜单状态重置动态路由会丢失页面直接404。解决办法是把菜单数据持久化到localStorage或者提供一个getUserInfo接口刷新时重新拉取并重新注册路由。axios封装是前端联调的关键一环。统一创建一个service实例import axios from axios; const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } return res; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } ElMessage.error(网络请求异常); return Promise.reject(error); } );把baseURL统一设置为/api开发环境通过Vite代理转发到后端生产环境由Nginx或后端本身处理转发。这样做的好处是以后接口地址变了只需要改代理配置前端代码不用动。4.2 核心页面实现的思路管理后台首页仪表盘用ECharts展示本周服务订单数、新增老人档案数、告警未处理数、服务完成率等指标。后端对应提供一个dashboard接口返回统计VO。ECharts配置其实不复杂难的是数据格式对齐。建议后端返回的统计字段直接跟前端图表series字段一一对应不要前端再二次加工减少出错概率。老人档案管理以表格为主顶部搜索栏按姓名、身份证、所属社区筛选。表格的“查看详情”弹窗里展示健康档案和最近服务记录。新增和编辑共用同一个表单弹窗提交后刷新表格数据。字段校验用Element Plus的表单rules身份证号、手机号要加正则校验。服务订单处理这是操作最频繁的页面。管理端主要看订单列表、查看详情、分配护工、处理取消申请。护工角色登录后看到的是“我的待办工单”接单时点按钮触发状态流转接口。实现上需要注意按钮的权限控制没有对应角色权限就通过v-if隐藏操作按钮前端隐藏只是体验问题真正的限制在后端接口鉴权两端都要做。健康数据可视化老人详情页放一个“体征趋势”Tab用ECharts折线图展示血压和血糖近30天的趋势。后端提供一个查询接口按老人ID和日期范围返回指标列表前端按时间排序后填充图表。这类图表对用户理解老人身体变化非常有帮助属于养老平台的加分项。4.3 前后端联调与本地代理配置开发阶段最大的痛点往往是跨域。前端跑在5173端口后端跑在8080端口直接请求必然触发CORS。解决办法是在Vite配置文件里加proxyexport default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这样前端请求/api/login会被代理到http://localhost:8080/login浏览器视角里是同源请求跨域问题直接消失。很多初学者在这里去后端写CrossOrigin或者配置CorsFilter绕了一大圈其实开发环境用Vite代理就够了。后端做一层CORS配置也没坏处尤其以后要接小程序和App但主力开发调试还是靠代理。联调阶段的接口文档建议直接用Apifox或Knife4j生成不要再手写Markdown文档。后端把Swagger注解配好前端在Apifox里直接看参数和返回示例效率能提升一大截。项目如果时间紧也可以在application.yml里关掉Swagger的production环境暴露防止线上接口文档泄漏。5. 常见问题与排查技巧实录5.1 MySQL连接报错与配置问题报错一com.mysql.cj.exceptions.InvalidConnectionAttributeException。原因是数据库URL缺少serverTimezone参数或者驱动版本与MySQL版本不匹配。解决方法是把URL改为jdbc:mysql://localhost:3306/pension_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueMySQL 8.x的认证插件是caching_sha2_password连接时还要加allowPublicKeyRetrievaltrue否则会报Public Key Retrieval is not allowed。报错二Communications link failure。最可能的原因是MySQL没启动或者MySQL端口不是默认的3306。先把netstat -ano | findstr 3306查一下端口再检查账号密码是否正确。如果是远程数据库还要确认服务器的安全组有没有放行3306端口。报错三Expression #1 of SELECT list is not in GROUP BY clause。这是MySQL 5.7以上开启了only_full_group_by模式对GROUP BY的字段严格要求。SQL里SELECT的非聚合字段必须出现在GROUP BY中。临时解决办法是关闭模式但长远看还是得调整SQL把不满足要求的字段去掉或改成GROUP_CONCAT、MAX等聚合函数。5.2 MyBatis经典报错排查MyBatis项目里最高频的报错就是Invalid bound statement (not found)。出现这个报错先按顺序排查Mapper接口方法名和XML里id是否完全一致包括大小写。XML文件是否放在了mapper-locations配置的目录下。XML文件里的namespace是否对应该Mapper接口的全限定名。target/classes目录下有没有编译进去XML文件如果没进去多数是resources目录被IDEA忽略了XML需要在pom或maven-resources-plugin里配置包含*.xml。第二个常见问题是参数绑定错误。Mapper方法声明了多参数时XML里必须用Param(xxx)声明名称否则MyBatis没法自动识别。建议所有多参数Mapper方法统一加上Param注解规避歧义。第三个是缓存导致的数据不刷新。MyBatis默认一级缓存是SqlSession级别的同一个SqlSession内相同的SQL会复用缓存。分页插件、多数据源切换等场景下偶尔会查出旧数据。这时候在更新方法后面手动清缓存或者把需要实时查询的Mapper方法加上flushCache选项。实际上大多数场景不需要做二级缓存关闭反而是最省心的做法。5.3 SpringBoot启动与打包问题端口被占用是启动时最常见的失败原因。直接把server.port改成8081或者杀进程。Windows下查找占用端口的命令netstat -ano | findstr 8080 taskkill /F /PID 进程号SpringBoot 3.x版本太高导致启动失败。一些旧依赖的内部实现基于JDK8的反射机制在JDK17版本下会报模块访问错误。之前我提到过尽量用SpringBoot 2.7和JDK8就是为了减少这类兼容性问题。如果非要用新版就同步升级MyBatis starter和相关依赖版本。打包打不出可执行jar。检查pom.xml里有没有spring-boot-maven-plugin这个插件是生成可执行jar的关键。打包命令mvn clean package -DskipTests打完后target目录下出现pension-0.0.1-SNAPSHOT.jar直接java -jar运行即可。5.4 Vue开发中的常见问题跨域请求失败。开发环境请先确认Vite代理配置生效请求路径中的/api前缀是否被正确转发了。生产环境直接部署时要让Nginx把/api开头的请求代理到后端地址同样能解决跨域。把前端打包成dist后放进SpringBoot的static目录也是一种部署方式但路由的history模式需要配置转发否则刷新页面会404。路由跳转404。使用history模式时服务器上必须配置所有非静态资源请求都返回index.html否则用户直接访问某个子路由会404。Nginx写法location / { try_files $uri $uri/ /index.html; }npm install太慢或安装失败。换国内镜像源是最直接的方案.npmrc里配置registryhttps://registry.npmmirror.com。如果还是报错删除node_modules和package-lock.json重新install。Node版本建议16以上Vite 4以上版本在低版本Node下会直接不支持。5.5 页面查询慢与系统卡顿优化养老平台数据量上来以后最容易出现查询慢的两个地方订单列表和健康档案查询。第一优先级加索引ALTER TABLE service_order ADD INDEX idx_elder_id (elder_id); ALTER TABLE service_order ADD INDEX idx_status_create_time (status, create_time); ALTER TABLE elder_health_record ADD INDEX idx_elder_record_time (elder_id, record_time);第二优先级做分页禁止一次性把全表数据查出来前端翻页。PageHelper配置里开启reasonable参数后页码越界能自动调整不至于因为前端传page9999导致查询极其缓慢。第三优先级是减少大字段返回。比如老人档案表里的备注字段列表页用不到就不要SELECT出来可以在DTO查询中明确字段。MySQL Innodb引擎下大字段会额外占用行数据空间扫描开销会明显上升。6. 项目复现步骤与部署优化6.1 数据库初始化与演示数据拿到源码后第一步永远是初始化数据库。用Navicat或者命令行工具执行mysql -u root -p pension_db.sql执行完之后检查关键表数量和数据量。至少应该有的表sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu、elder_info、elder_health_record、service_item、service_order、service_order_worker、alert_record。初始化数据至少要包含管理员账号admin / 123456三个角色超级管理员、护工、社区管理员5个演示老人档案其中至少一个健康等级是失能方便测试特殊服务流程4个服务项目上门助浴、代购药品、陪同就医、日常保洁如果.sql脚本里没有这些数据建议自己补上否则前端页面打开全是空白很多功能没法验证。6.2 本地运行后端后端导入IDEA后先等Maven把依赖下完。然后检查三处配置application.yml里数据库账号密码是否正确。MySQL端口是否是3306不是就同步修改URL。本地JDK版本是否与项目pom里java.version一致。启动类PensionApplication直接main运行。控制台出现“Started PensionApplication in xx seconds”后浏览器访问http://localhost:8080/swagger-ui/index.html或接口文档页面验证服务是否可用。没配Swagger的话直接访问登录接口返回JSON就表示OK。6.3 本地运行前端前端启动流程npm install npm run dev第一次install如果比较慢建议先配置镜像源。dev启动后浏览器打开http://localhost:5173能看到登录页说明前端起来正常。输入初始化数据里的管理员账号能成功登录并跳转到首页说明前后端联调通了。联调不通时优先看浏览器F12的Network面板。请求返回404检查代理配置和后端地址请求返回401检查token有没有带上请求返回500去看后端控制台报错日志。6.4 生产部署与优化建议线上部署有两种常用方式选哪种取决于团队能力和服务器资源。方式一前后端分离部署。后端打jar包前端打dist静态文件用Nginx同时托管前端页面和反向代理后端接口。这个方案灵活前端静态文件可以走CDN加速后端可以单独扩节点。基本部署配置server { listen 80; server_name your-domain.com; root /opt/pension-admin/dist; index index.html; location / { try_files $uri $uri/ /index.html; } 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; } }方式二前后端合并部署。前端打包后的dist目录复制到SpringBoot的src/main/resources/static目录下重新打包成一个jar由SpringBoot统一提供页面和接口。这种方案运维最简单只需要一个jar进程但前端一改版就得重新打包jar适合项目早期或演示阶段。Docker化是推荐的做法。写一个Dockerfile基础镜像用eclipse-temurin:8-jre把jar放进去暴露8080端口。MySQL单独跑容器或者用云数据库不要把数据和应用混在一个容器里。生产环境的数据库密码通过环境变量注入日志挂载到宿主机方便排查线上问题。7. 实际开发中的经验沉淀做养老智慧服务平台这类系统技术难点从来不在某个框架的API上而在于业务上的耐心和细致。我实际做完一版之后有几个感受想分享。第一需求里“智慧”两个字最容易让人跑偏。我刚规划系统时也想加人脸识别、智能语音播报、大屏可视化这些看起来很高级的功能后来发现核心用户根本用不上。社区工作人员每天打开系统的目的非常明确看今天谁需要上门服务、谁的健康有异常、哪些工单还没处理。把这三个事情做到极致比一百个炫酷功能都管用。所以设计功能时始终要问一句这个功能解决谁的什么问题如果回答不出来就先不做。第二状态机设计千万不能省。订单状态、告警状态、工单状态一定要在数据库层面有明确的字典或枚举定义并且在前端展示时做状态标签映射。我遇到过一种情况某同事直接在前端写“如果status2显示已完成status3显示已取消”结果后端后来改了枚举含义前端没有同步页面显示完全错乱。前后端统一维护一份状态常量表是最省心的办法。第三权限设计不能等系统上线后补。养老平台涉及健康档案和老人联系方式这些数据按照个人信息保护的要求是需要严格限制访问范围的。从一开始就把RBAC做好哪怕初期角色少一点也没关系。后面加运营人员、加第三方监管账号时直接在角色菜单表里加数据就行同时在我做接口鉴权时对每个敏感操作都把关比如只有养老护理员角色以上才能查看老人身份证号其他岗位只能看到姓名和联系方式实现上其实就是在查询SQL里做一个字段白名单控制。第四测试数据要贴近真实业务。很多人做演示数据时老人名字随便起“张三李四”服务项目随便填“服务1服务2”。真到了演示和验收阶段这种数据会给用户一种系统很敷衍的感觉。建议每个演示老人都设计成有故事感的档案张阿姨78岁独居患有高血压每周三需要代购药品李叔叔85岁半失能每月需要助浴两次。业务数据一丰满系统给人一种被认真运营过的感觉演示效果立竿见影。第五Backup和日志要提早规划。养老平台的数据是真正关系到人的健康和安全的数据库备份策略必须上线前就确定。至少要做到每日全量备份备份文件异地存放或传到对象存储同时定期做恢复演练。日志方面操作日志跟业务日志分开至少保留90天。系统出问题不可怕可怕的是出问题后连当时发生了什么都不知道。最后再说一个习惯。我在实现完每个模块后都会模拟最笨的用户去操作一遍登录的时候密码大小写写错会不会有提示删除老人档案时有没有二次确认弹窗查不到数据时前端显示的是不是让人舒服的空状态这些细节打磨到位了系统的完成度才能真正从“跑通”变成“好用”。做养老平台这种面向弱势群体的系统尤其如此界面上的一个词、操作上的一个按钮都可能直接影响工作人员每天的使用体验。多站在使用者的角度想想这个项目才算真正做完。