☰
SpringBoot+Vue智慧养老服务平台源码解析:从数据库设计到前后端部署
2026/10/5 2:49:24 网站建设 项目流程

1. 项目整体设计:为什么是SpringBoot+Vue,而不是别的

1.1 先搞清楚养老智慧服务平台到底在管什么

很多人第一次听到“养老智慧服务平台”,脑子里会跳出一堆概念:智能手环、人脸识别门禁、呼叫中心、大屏指挥……这些确实是智慧养老的组成部分,但如果只盯着硬件噱头去做系统,最后往往会变成一个“看得到但用不起来”的演示项目。

我拿到这个标题的第一反应是先划边界。SpringBoot+Vue+MySQL+MyBatis这个组合,本质上交付的是一套业务管理系统,核心场景不是物联网设备接入,而是围绕“老人—家属—护工—平台管理员”这几类角色,把日常养老服务跑通:建档、评估、预约、派单、执行、回访、计费、健康数据沉淀。也就是先有服务流程的数字化,再谈智能硬件和大屏可视化。

和普通商城类管理系统相比,养老平台有几个明显特点:

  • 服务对象特殊。老人大多不会自己操作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好找得多。标题里着重提了Java+MySQL+MyBatis完整源码,说明这套组合的复制成本低,拿到源码往本地一导就能跑,这一点对学习型项目尤其重要。

为什么是Vue而不是React或Angular?国内后台管理系统的团队协作习惯里,Vue配套的Element Plus或Ant Design Vue组件库,能把表格、表单、弹窗、树形菜单这些后台高频组件开箱即用地组合起来。Vue的模板语法对后端转前端的开发者也更友好,路由和状态管理同样轻量。如果目标是快速做出一套整洁的管理界面,Vue是目前综合成本最低的选项。

1.3 源码工程的合理结构长什么样

一个合格的SpringBoot+Vue完整源码,至少应该包含三块:后端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不直接操作HttpServletRequest,Mapper只负责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:老人ID
  • alert_type:告警类型,1-SOS 2-体征异常 3-设备离线 4-长时间未活动
  • alert_content:告警内容描述
  • alert_time:告警发生时间
  • handler:处理人ID
  • handle_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_time
  • sys_role:角色表,字段包括role_id、role_name、role_code、remark
  • sys_menu:菜单/权限表,字段包括menu_id、parent_id、menu_name、path、perms、menu_type、icon、order_num
  • sys_user_role:用户角色关联表
  • sys_role_menu:角色菜单关联表

登录后把用户拥有的权限标识集合(perms)加载到内存,写一个自定义注解配合Spring AOP或拦截器做接口鉴权。例如:

@RequirePermission("pension:order:accept")

这样代码里每次涉及敏感操作时,一个注解就能控制访问范围,可读性和安全性都远比赛后在Controller里手动判断“当前用户的roleId是否等于1”要好。

MySQL设计上,关联表的主键最好是联合主键,比如sys_user_role的user_id+role_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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>

PageHelper这个插件我建议一开始就加上。后台管理系统的每个列表几乎都逃不过分页,手写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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 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,映射时就能省下大量冗余配置。

serverTimezone=Asia/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.active=prod

3.3 MyBatis映射文件与动态SQL写法要点

项目里最复杂的查询往往出现在管理台列表页。比如订单管理列表,需要按订单号模糊搜索、按老人姓名搜索、按服务项目搜索、按状态筛选、按时间范围筛选,还得关联出老人姓名、护工姓名、服务项目名称。这种场景正是写动态SQL的典型。

<select id="selectOrderList" resultType="com.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 test="orderNo != null and orderNo != ''"> AND so.order_no LIKE CONCAT('%', #{orderNo}, '%') </if> <if test="elderName != null and elderName != ''"> AND e.elder_name LIKE CONCAT('%', #{elderName}, '%') </if> <if test="status != null"> AND so.status = #{status} </if> <if test="startTime != null"> AND so.create_time &gt;= #{startTime} </if> <if test="endTime != 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 Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

对应地,全局异常处理器要捕获三类异常:业务异常(订单状态不允许操作)、参数校验异常(前端必填项没传)、兜底异常(数据库挂了之类的意外)。业务异常可以通过自定义BusinessException,在Service层主动抛出,异常处理器里统一返回code=500、message等于自定义提示。参数校验异常通常由@Valid触发,处理器里把字段错误信息拼装成中文提示返回给前端。

有个细节很多教程不会讲:Result.error里的message尽量不要直接透出数据库异常原文,例如“Duplicate entry 'xxx' for key 'id_card'”这种信息,直接给用户看既不友好,也可能泄露表结构。应该捕获唯一约束冲突后转化成“该身份证号已存在档案”。我在全局异常处理里专门对SQLIntegrityConstraintViolationException做了一次包装,实测下来前端提示体验提升非常明显。

3.5 登录认证与JWT

养老平台的登录认证,我建议用JWT而不是Session。原因是前后端分离后,后端接口会被PC管理端、移动端、小程序共用,Session在跨域和App场景下都不好维护,而JWT无状态,只要前端每次请求把token放Header里带着就行。

实现思路很简单:

  1. 登录接口接收username和password,校验通过后用JWT生成token,把用户ID和用户名编码进去,设置过期时间(建议2小时)。
  2. 写一个拦截器或过滤器,对所有除白名单外的接口做token解析。解析失败返回401,前端收到401后跳回登录页。
  3. 从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 vue@latest 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配置文件里加proxy:

export 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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

MySQL 8.x的认证插件是caching_sha2_password,连接时还要加allowPublicKeyRetrieval=true,否则会报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)。出现这个报错先按顺序排查:

  1. Mapper接口方法名和XML里id是否完全一致,包括大小写。
  2. XML文件是否放在了mapper-locations配置的目录下。
  3. XML文件里的namespace是否对应该Mapper接口的全限定名。
  4. 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里配置registry=https://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参数后,页码越界能自动调整,不至于因为前端传page=9999导致查询极其缓慢。

第三优先级是减少大字段返回。比如老人档案表里的备注字段,列表页用不到就不要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把依赖下完。然后检查三处配置:

  1. application.yml里数据库账号密码是否正确。
  2. MySQL端口是否是3306,不是就同步修改URL。
  3. 本地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上,而在于业务上的耐心和细致。我实际做完一版之后有几个感受想分享。

第一,需求里“智慧”两个字最容易让人跑偏。我刚规划系统时,也想加人脸识别、智能语音播报、大屏可视化这些看起来很高级的功能,后来发现核心用户根本用不上。社区工作人员每天打开系统的目的非常明确:看今天谁需要上门服务、谁的健康有异常、哪些工单还没处理。把这三个事情做到极致,比一百个炫酷功能都管用。所以设计功能时始终要问一句:这个功能解决谁的什么问题,如果回答不出来,就先不做。

第二,状态机设计千万不能省。订单状态、告警状态、工单状态,一定要在数据库层面有明确的字典或枚举定义,并且在前端展示时做状态标签映射。我遇到过一种情况:某同事直接在前端写“如果status==2显示已完成,status==3显示已取消”,结果后端后来改了枚举含义,前端没有同步,页面显示完全错乱。前后端统一维护一份状态常量表,是最省心的办法。

第三,权限设计不能等系统上线后补。养老平台涉及健康档案和老人联系方式,这些数据按照个人信息保护的要求是需要严格限制访问范围的。从一开始就把RBAC做好,哪怕初期角色少一点也没关系。后面加运营人员、加第三方监管账号时,直接在角色菜单表里加数据就行,同时在我做接口鉴权时对每个敏感操作都把关,比如只有养老护理员角色以上才能查看老人身份证号,其他岗位只能看到姓名和联系方式,实现上其实就是在查询SQL里做一个字段白名单控制。

第四,测试数据要贴近真实业务。很多人做演示数据时,老人名字随便起“张三李四”,服务项目随便填“服务1服务2”。真到了演示和验收阶段,这种数据会给用户一种系统很敷衍的感觉。建议每个演示老人都设计成有故事感的档案:张阿姨,78岁,独居,患有高血压,每周三需要代购药品;李叔叔,85岁,半失能,每月需要助浴两次。业务数据一丰满,系统给人一种被认真运营过的感觉,演示效果立竿见影。

第五,Backup和日志要提早规划。养老平台的数据是真正关系到人的健康和安全的,数据库备份策略必须上线前就确定。至少要做到每日全量备份,备份文件异地存放或传到对象存储,同时定期做恢复演练。日志方面,操作日志跟业务日志分开,至少保留90天。系统出问题不可怕,可怕的是出问题后连当时发生了什么都不知道。

最后再说一个习惯。我在实现完每个模块后,都会模拟最笨的用户去操作一遍:登录的时候密码大小写写错会不会有提示,删除老人档案时有没有二次确认弹窗,查不到数据时前端显示的是不是让人舒服的空状态,这些细节打磨到位了,系统的完成度才能真正从“跑通”变成“好用”。做养老平台这种面向弱势群体的系统尤其如此,界面上的一个词、操作上的一个按钮,都可能直接影响工作人员每天的使用体验。多站在使用者的角度想想,这个项目才算真正做完。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询