前阵子帮一个在校生做项目评审题目就是“基于SpringBootVue的社区养老服务平台管理系统设计与实现【JavaMySQLMyBatis完整源码】”。那位同学一开始挺自信数据库建了二十多张表接口也写了将近两百个可等我打开系统一看登录页倒是出来了点进去之后页面跟数据完全对不上。问题不是代码量少而是对“社区养老”这个业务模块的边界没想清楚。这篇文章我不打算逐行晒源码而是把拿到这个题目后从需求分析、数据库表设计、后端接口、前端页面再到部署冒烟测试的完整思路走一遍。目的是让你在写代码之前先答得上来系统到底管什么、为什么这样建表、接口和页面怎么对接上。思路顺了代码只是时间问题。1. 先搞清楚这个系统到底要管什么1.1 别急着建表先把“用系统的人”摆出来很多同学拿到题目第一反应是打开数据库工具建表这是最容易翻车的做法。社区养老服务平台的核心不是“报一个老年人口数字看板”而是要让三类人通过同一个系统协作起来。我从需求层面把角色分成三类后面所有功能和表都是围着这三类人转的角色核心诉求使用方式平台管理员管人、管服务、看数据。维护老人档案审核工单分配服务人员统计服务完成情况PC后台管理端服务人员/护工接收工单查看老人基础信息和注意事项上门服务后填报结果PC后台或移动H5老人及家属查询服务进度、健康记录、历史工单以查看为主多数场景走家属代办如果你把这三类人的对话串成一条线业务其实很清晰管理员录入老人档案老人产生服务需求管理员创建服务工单护工接单并上门服务完成后生成回访记录和健康数据。这就是一个典型的服务流转闭环和外卖平台的“建店—下单—接单—配送—评价”模型很像只是把商品换成了助餐、助洁、助医等养老服务。1.2 从业务痛点倒推功能清单社区养老系统的痛点拆成三层来看第一层是基础信息管理。老人的联系方式、紧急联系人、基础疾病、自理能力、居住地址原来都是纸质台账查询和更新都很痛苦。系统要解决的核心问题就是把档案电子化并且能快速检索。第二层是服务流程管理。服务不是“今天有人上门就行”得有人创建工单、有护工接单、有服务开始时间和完成时间、有回访记录。没有流程管理养老服务就停留在口头安排出了纠纷也说不清楚。这里需要一个完整的工单状态机。第三层是数据辅助决策。街道或者社区想知道的不是单个人的情况而是“这个月助餐服务做了多少次”“有多少老人健康数据连续三个月在血压高位”。系统要能做基础统计支撑后续的补贴发放、服务资源调配。1.3 划清项目边界不要给自己挖坑任何“设计与实现”类项目最怕的不是功能少而是功能多到没法收尾。我这个标题下的系统核心闭环就四个模块老人档案管理、服务工单管理、健康记录管理、系统权限管理。外部物联网设备对接、移动端App、GPS定位、视频通话、在线支付这些都不要放进第一版。不是说这些功能没用而是你要在答辩时讲清楚“为什么不做”。我通常建议在数据库里预留扩展位比如健康记录表预留一个device_id字段但界面上不展示。这样既保住了开发进度也显得你考虑过扩展性而不是压根没想到。2. 技术选型不跟风这套组合为什么这么搭2.1 SpringBoot解决的是“配置地狱”问题题目指定SpringBoot不是没有道理的。前几年用SSM做同类项目光applicationContext.xml、spring-mvc.xml、web.xml就能让新人纠结一周。SpringBoot的核心价值是用自动配置把大部分模板代码沉淀掉让你把精力放在业务逻辑上。但有两个容易被忽略的关键点。第一是版本匹配。如果你的JDK还是8或11不建议用SpringBoot 3.x因为3.x强制要求JDK17很多本地环境和教学模板还是1.8跑着。稳妥组合是JDK8 SpringBoot 2.7.x兼容性最好网上资料最多遇到问题能快速搜到答案。第二是不能完全依赖自动配置。数据库连接、端口、MyBatis的mapper扫描路径、文件上传大小限制这些依然要在application.yml里明确写上。很多同学“SpringBoot项目启动失败”的报错其实就是漏了数据源参数。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/community_care?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.communitycare.entity2.2 Vue Element UI是后台管理项目的现实之选前端技术栈为什么要用Vue因为社区养老平台本质上是一个“管理后台”核心页面是表格、表单、弹窗、统计卡片。Vue的单文件组件和响应式数据绑定让这种CRUD密集型页面开发效率非常高。这里要提醒版本问题如果题目只写“Vue”市面上大量课程和模板还是Vue 2.6 Element UI的组合而不是Vue 3 Element Plus。Vue2和Vue3在生态上差别非常大很多同学下载一个Vue3的模板结果照着一本Vue2教材写半天跑不通。我给你的建议是先确认你的基础。如果你跟着的教程是Vue2项目就用Vue2如果你已经会Vue3的setup语法就选Vue2项目中常用的组件库。核心目的不是追新而是让前后端能顺利联调完毕。技术栈版本优点注意点Vue 2.6 Element UI案例最多、组件踩坑记录多适合课程设计节奏2023年后官方已停止维护Vue 3.2 Element Plus组合式API更优雅长期维护有保障部分组件用法与Element UI不同模板少一些2.3 MyBatis和MySQL图的就是可控和简单这套项目用MyBatis配合MySQL最大的好处是SQL写法非常直观。MyBatis不像Hibernate那样有很强的对象关系映射也不容易产生“你明明只查一条记录它却给你发了一堆关联SQL”的尴尬情况。你自己写的SQL出了性能问题自己也能看得懂。有人会问为什么不用MyBatis-Plus我的回答是如果题目允许实际工程完全可以加它能把单表CRUD代码缩短很多。但既然题目明确写了MyBatis你核心的增删改查最好还是用原生SQL完成至少在答辩时你能解释清楚“这个SQL是在查什么、为什么这么写”。结合使用也是可以的但不要让你的项目里一眼望去全是Plus的LambdaQueryWrapper而看不到一条自己写的SQL。3. 功能模块拆解从老人档案到服务工单3.1 系统管理模块先跑通权限闭环我见过太多项目急着写老人列表最后在登录权限上彻底翻车。系统管理这个模块虽然看起来不是业务亮点但它是整个系统的地基。推荐使用经典的RBAC模型用户—角色—菜单三层关联。也就是一个账号可以被分配一个或多个角色一个角色关联一组菜单权限。登录之后后端根据用户ID查出角色再根据角色查出菜单前端根据返回的菜单树动态生成侧边栏。这里有三个很实在的设计细节菜单表里要有parent_id字段用来做父子级菜单一级菜单是“系统管理”“老人管理”“服务工单”“健康记录”二级菜单才是具体的页面。按钮级的权限可以不做但是菜单级权限一定要做。否则任何一个小管理员登录后看到的菜单和管理员一模一样这个模块就没有意义了。密码不能明文存。至少用MD5加盐或者BCrypt加密。如果你在答辩的时候被问“数据库被拖走了怎么办”这一步就是你的唯一防线。3.2 老人档案模块不是简单地写一个“老人表”老人档案是整个系统的数据基础。字段上除了姓名、性别、身份证号、联系电话这些常规项一定要有这几个业务字段紧急联系人及电话养老服务上门过程中一旦出现异常必须快速找到家属。健康标签例如“高血压、糖尿病、行动不便”可以用逗号分隔的字符串存储。自理能力评级从“完全自理”到“完全不能自理”这直接影响服务工单的派单难度和时长。居住地址要尽量写得具体因为后面派单时护工是按照区域接单的。为什么要用逗号分隔字符串存储健康标签而不是单独建一个“老人标签中间表”因为本系统的核心使用场景是列表展示标签而不是拿标签做复杂的组合检索。如果你建三张关联表来做这个反而把简单问题复杂化查询速度也没有提升。3.3 服务工单模块核心是状态机服务工单是本系统业务价值最高的模块建议把重点放在这里。工单从创建到归档状态必须清晰状态值状态名称可操作角色说明0待派单管理员工单刚创建未分配给护工1已派单管理员/护工已指定护工等待接单确认2服务中护工护工开始上门服务3已完成护工服务结束填写服务反馈4已回访管理员管理员回访完成工单归档这五个状态背后有一个强烈的逻辑约束状态不能随意跳转。比如“待派单”不能直接跳到“服务中”必须先派单“已完成”不能直接回到“待派单”。把状态流转规则写在Service层前端只负责调用接口不自己改状态。这样哪怕是用户频繁点击按钮、并发请求数据也不会乱。一个工单的基本流程是管理员根据老人的需求比如助餐、助洁创建工单选择服务类型指定护工护工接单后上门完成后填写服务内容和耗时最后管理员电话回访记录满意度。这一套走下来项目的核心难点和答辩亮点就都有了。3.4 健康记录模块做简单但要有分析价值健康记录不要做得太重因为社区养老系统的健康数据来源是护工上门测量或者老人家属手动填报不是医院级别的数据。一张health_record表就够了老人ID、收缩压、舒张压、空腹血糖、心率、记录日期、记录人。这个模块可以做成一个“最近30天趋势图”。后端把查询参数传给前端前端用图表组件画折线。比如一位老人的血压连续几次超过140/90前端就提示“血压偏高”。这种分析不需要任何算法就是普通的区间判断但用来做项目展示非常讨巧也呼应了“健康管理”的业务目标。4. 数据库设计九张表怎么建才不返工4.1 先画数据关系再写建表语句我在做这类项目时习惯先确定表清单再动SQL。这个系统我给出一套比较舒服的表结构九张表其中七张是核心主表两张是关联表表名类型作用sys_user主表系统登录账号区分管理端账号和服务人员账号sys_role主表角色定义比如系统管理员、服务人员sys_menu主表菜单权限定义sys_user_role关联表用户和角色多对多关联elder_info主表老人档案核心表service_type主表服务项目目录如助餐、助洁、助医、代办、陪伴service_order主表服务工单主表记录一次服务全流程health_record主表老人的健康测量记录service_follow主表工单回访记录表为什么不额外建老人和家属的独立账户表因为这个系统第一版面向的是管理员和护工老人及家属更多是“被服务对象”而非主动登录用户。如果把老人也做成登录用户就要牵扯注册、验证码、微信绑定工作量直接上一个台阶。边界就是这么划出来的。4.2 核心建表SQL字段名和注释一样不能少很多人建表时省注释到写代码时到处翻。我建议用Navicat或MySQL Workbench建表时把COMMENT写在字段后面。这里以service_order为例你看一眼就明白工单表的设计要点CREATE TABLE service_order ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 工单ID, order_no VARCHAR(32) NOT NULL COMMENT 工单编号如SG20250801XXXX, elder_id INT NOT NULL COMMENT 老人ID关联elder_info, elder_name VARCHAR(50) NOT NULL COMMENT 冗余老人姓名减少联表查询, service_type_id INT NOT NULL COMMENT 服务项目ID, service_type_name VARCHAR(50) NOT NULL COMMENT 冗余服务项目名称, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待派单 1已派单 2服务中 3已完成 4已回访, assign_user_id INT COMMENT 派单护工ID, assign_time DATETIME COMMENT 派单时间, start_time DATETIME COMMENT 服务开始时间, end_time DATETIME COMMENT 服务结束时间, service_address VARCHAR(255) COMMENT 服务地址, service_content VARCHAR(500) COMMENT 服务内容描述, feedback VARCHAR(500) COMMENT 护工服务反馈, satisfaction TINYINT COMMENT 回访满意度1-5分, follow_user_id INT COMMENT 回访人ID, follow_time DATETIME COMMENT 回访时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0正常 1删除 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT服务工单表;这里有一个很多教程不会教你的设计经验工单表里冗余了elder_name和service_type_name。为什么不直接关联查因为工单列表页需要显示老人姓名和服务项目名如果每次都三表联查SQL写起来麻烦索引消耗也大。对于读写比例悬殊的系统读多写少冗余两个字段时间换空间非常值。你只需要在创建工单和老人改名的接口里同步更新一下即可这种低频维护成本完全可以接受。4.3 逻辑删除和审计字段从第一天就加上我建议每张业务表都带上deleted、create_time、update_time三个字段。逻辑删除的意思是用户点击“删除老人档案”时不执行物理DELETE而是UPDATE elder_info SET deleted 1 WHERE id ?。这样做的原因有三个老人档案是敏感数据误删之后无法恢复物理删除风险太大工单表的外键指向老人档案逻辑删除不会导致历史工单查询报错或关联缺失答辩的时候你可以解释这是“数据安全性的考虑”属于加分项。5. 后端实现要点写接口前必须想清楚的几件事5.1 统一返回结果省去无数次前端联调前后端分离项目最忌讳每个接口返回结构都不一样。这个接口返回{code:1, msg:ok}那个接口返回{status:200, message:success}前端根本没法写通用逻辑。我在项目中会定义一个ResultT类所有接口统一返回public class ResultT { private Integer code; // 200成功500业务异常401未登录 private String message; // 提示信息 private T data; // 具体数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }配合RestControllerAdvice全局异常拦截业务里只需要抛出自定义异常前端收到统一格式弹message字段里的提示就行。这个设计能让你和前端配合时少吵很多架。5.2 分页查询的完整链路SQL和参数别搞混老人列表、工单列表、回访列表每个列表都需要分页。分页的链路是前端传pageNum第几页和pageSize每页多少条到ControllerController转给ServiceService把参数传给MapperMapper在SQL里用LIMIT offset, pageSize截取数据最后返回一个包含list和total的PageResult。很多人容易忽略的是offset的计算offset (pageNum - 1) * pageSize。如果用户在第2页每页10条就应该从第10条开始取10条也就是LIMIT 10, 10。前端拿到的总条数total也需要在SQL里单独SELECT COUNT(*)查出来。这里千万不要用SELECT *然后去List.size()当总数那样数据量一大必然出问题。5.3 JWT登录鉴权够用就好登录方案我用JWT不引入Spring Security或Shiro。理由是这个项目算上菜单权限用拦截器加注解就能覆盖引入重量级安全框架反而增加学习成本和配置复杂度。具体实现思路是这样的用户登录成功后后端生成一个token里面用userId作为自定义声明。前端每次请求在请求头里带上Authorization: Bearer xxx。后端写一个拦截器放行登录接口其余接口一律校验tokentoken有效就从里面取出userId放到ThreadLocal或请求参数里供Controller使用。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); 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; } }有一点要注意前端登录接口在白名单里/api/login放行但用户信息接口/api/user/info必须登录后才能拿到。如果放行得太宽整个鉴权形同虚设。5.4 MyBatis里最常见的几个坑MyBatis的SQL写起来直观但有几个点特别容易踩我每次做项目都会盯一遍#{}和${}别混用。#{}是预编译占位符会处理成?安全防注入${}是字符串替换通常只用于动态排序列名比如ORDER BY ${sortField}。传参数过滤条件一律用#{}。模糊查询的写法。MySQL里不能直接写LIKE %#{keyword}%会报错。正确写法是LIKE CONCAT(%, #{keyword}, %)。批量插入用foreach。如果你在循环里逐条调用单条insert几百条数据能卡到你怀疑人生。用MyBatis的foreach标签一次插入效率差距是几十倍。Map或实体类的param命名。多参数传递时建议用Param注解否则SQL里的参数名容易对不上。6. Vue前端对接别把后端数据直接塞进vuex6.1 前端目录结构一上来就按项目化组织前端代码如果你全堆在App.vue里写到后面基本没法维护。我按这个结构组织项目src/ api/ // 按模块封装的接口请求如 elder.js、order.js views/ // 页面组件如 elder/List.vue、order/List.vue router/ // 路由配置动态路由和静态路由 store/ // 全局状态只放用户信息和菜单 utils/ // axios封装、时间格式化等工具 layout/ // 后台整体布局api/目录下的每个文件对应一个后端Controller。比如elder.js里封装了getElderList、addElder、updateElder、deleteElder四个函数每个函数返回一个axios请求。好处是页面组件里不直接出现url之后后端改地址只需要改一个文件。6.2 表格和分页的联动是CRUD页面的标准套路社区养老系统里老人列表页面算是“前台”工单列表页面也跟它长得差不多。核心套路是页面初始化时执行fetchList()参数是queryParams对象。表格的el-pagination组件触发current-change时更新queryParams.pageNum再调用fetchList()。搜索按钮触发时把pageNum重置为1保证搜索后从第一页开始看。const queryParams { pageNum: 1, pageSize: 10, keyword: } async function fetchList() { const res await getElderList(queryParams) if (res.code 200) { tableData.value res.data.list total.value res.data.total } }很多人写到这里会把整个tableData放到vuex里管理这其实是个坏习惯。表格的临时数据刷新一次就变了不存在跨页面共享的必要。vuex里真正该放的是用户信息和动态菜单因为侧边栏需要全局读取而页面刷新后vuex内容会丢失所以还要在路由守卫里根据用户ID重新拉菜单。6.3 新增和编辑复用同一个Dialog少写一堆重复代码老人档案的新增和编辑弹窗内容几乎一样。很多同学会复制粘贴两个对话框组件改半天还容易漏字段。正确做法是一个el-dialog用formTitle变量控制标题提交时根据form.id是否存在判断是新增还是编辑。保存成功后关掉弹窗重新调用fetchList()刷新表格。这里有一个非常实用的细节表单校验规则在校验失败时this.$refs.form.validate()返回的Promise会reject导致后续代码不执行。所以在then里写提交逻辑即可。7. 部署与实测启动后最容易翻车的三个坑7.1 坑一中文乱码排查顺序很关键数据库里的中文、页面传来的中文全部变成???这个问题在前后端分离项目里尤其常见。排查顺序有三种先看数据库连接串有没有characterEncodingutf8再看数据库表和字段的字符集是不是utf8mb4最后看前端axios请求有没有设置Content-Type: application/json;charsetUTF-8一般axios默认会带。这里要特别说明utf8mb4和utf8在MySQL里不是一回事。utf8最多存储3字节遇到生僻字或者Emoji会乱码直接用utf8mb4最稳妥。7.2 坑二跨域问题浏览器把请求拦了前端跑在http://localhost:9528后端跑在http://localhost:8080端口不同就存在跨域。前端代理是一种方案后端直接配置CORS更省事Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }CORS配置好以后注册的拦截器要记得放行OPTIONS预检请求否则前端发送复杂请求时会被拦截器当场拦截报401错误。这个细节我在代码示例里已经体现if (OPTIONS.equals(request.getMethod())) return true;。7.3 坑三打包后页面刷新404前端开发模式下一切正常但npm run build之后把dist丢进Nginx或者和SpringBoot打成一起一刷新页面就404。原因通常是前端路由用的是history模式。这种模式下浏览器地址栏里的路由是假的比如/system/user后端没有对应的资源自然404。最省心的做法是直接用hash模式创建路由const router new VueRouter({ mode: hash, routes })地址栏会变成/#/system/user。Hash模式下刷新页面不会请求服务器也就不存在404问题。如果你非要保留history模式就需要在后端做路由重写把无法匹配的路径转发到index.html这个配置和你的部署方式强相关对课程设计来说性价比不高。7.4 验收清单别等到答辩才发现功能缺一角最后送上我跑这类系统的冒烟测试清单照着过一遍基本就能安心了功能场景操作步骤预期结果登录输入用户名密码登录成功跳转首页并显示动态菜单权限控制用服务人员账号登录不显示“用户管理”菜单老人新增填写带身份证号的档案后端校验身份证格式通过后列表出现新记录老人搜索按姓名关键词搜索列表显示匹配结果分页total正确创建工单选择老人和服务类型生成待派单状态工单派单管理员选择护工工单状态变为已派单护工账号能看到该工单服务完成护工填写反馈提交工单状态变为已完成回访管理员填报满意度和回访内容工单状态变为已回访无法再修改健康记录添加一条老人血压数据趋势图出现新的数据点账号停用管理员停用护工账号该护工无法登录系统说实话这类系统真正的难点不在新技术而在状态和数据的一致性。我在实际带A同学跑这个项目的过程中最深的体会是先画清楚工单状态流转图再动手建表比什么都重要。工单走完了档案和健康记录都只是另外两张基础表的问题。如果你正好拿到同学题的后续版本比如还需要加志愿者管理、活动报名这套表结构往上扩展也不吃力——核心的流程和数据模型不要推倒重做后续加模块就是加表、加菜单、加页面的事。希望你做完之后也能把每个字段为什么存在讲得明明白白。
阅读完成 · 觉得有帮助?