简介一份基于微信小程序与SSM框架的社区养老服务系统毕业设计源码案例适合计算机相关专业学生用于毕设、课程设计或期末大作业也适合想积累项目实战经验的学习者。系统覆盖预约护理、健康管理、日常照料和文化活动等功能完整呈现小程序前端与Spring/SpringMVC/MyBatis后端的整合思路模块划分清晰、便于二次开发。压缩包共1709个文件大小约88.39MB其中273个js脚本、218个java类承担前后端核心逻辑160个vue页面、144个wxss样式和142个wxml模板负责界面展示并附sql数据库脚本、mp4演示视频、使用说明及环境安装说明便于从零复现运行环境。目前已有79人学习下载。项目源码经过导师指导与调试可顺畅运行配合演示视频和文档能快速梳理业务模块与SSM框架落地方法还可学习数据表与接口设计为社区养老信息化设计提供高质量参考。1. 小程序SSM 做社区养老系统为什么这个方向值得认真做微信小程序社区养老服务系统前端跑在微信里后端由 SSMSpringSpringMVCMyBatis支撑是毕业设计里实用性和完成度都比较高的一个方向。它的核心价值在于把家属预约助餐、保洁、陪医等服务管理员派单、服务人员接单、老人健康档案管理串成一条完整业务链。这个系统难的不是某个技术点而是角色多、状态多、流程长能把这些理清楚并完整跑通答辩时天然有东西可讲。适合正在选毕业设计题目、或者想系统性掌握小程序SSM 全栈开发的同学参考。2. 系统拆解与技术选型养老服务业务流程和 SSM 后端的边界划分在动手写代码之前先把业务边界划清楚。很多同学翻车不是因为代码写不出来而是不知道哪些逻辑该放前端、哪些放后端、哪些放数据库最后代码全揉在 Controller 里改一个功能动全身。这一章先把养老系统的业务模块拆开再说 SSM 三层各自管什么最后理一条完整请求链路出来。2.1 养老服务系统的四个核心业务模块与角色权限设计社区养老服务系统本质上是一个服务预约平台围绕“服务”二字展开。我一般会把业务拆成四个模块。第一个是用户与角色模块系统里至少有三类角色老人或家属负责下单服务人员负责接单执行管理员负责派单和审核。设计上不需要给每类角色单独建表用一张 user 表加 role 字段区分即可登录后根据 role 返回不同的首页或菜单。第二个是服务项目管理模块助餐、保洁、陪医、陪伴这几类服务每类有名称、价格、详情、上下架状态。这个模块逻辑最简单但上下架状态必须做否则管理员没法临时停掉某个服务也没法解释为什么用户还能下单一个已经停掉的项目。第三个是订单流转模块这是整个系统的核心。订单从用户提交开始经历待接单、已接单、服务中、已完成、已取消这几个状态。状态不能想怎么跳就怎么跳比如已取消的订单不能再变成已完成这块建议在 Service 层做状态流转校验。第四个是健康档案模块家属可以定期帮老人记录血压、心率等数据服务人员在陪医服务时也能参考。这个模块虽然代码量不大但在答辩时很能体现“需求分析”的完整性。风险点在于老人信息涉及隐私项目里要注意脱敏展示前端列表不要直接显示完整手机号。角色权限上用一张表说明常见的划分角色查看范围核心操作对应端老人/家属服务列表、自己的订单、健康档案下单、取消订单、填健康数据微信小程序服务人员派给TA的订单接单、更新服务状态小程序/管理端管理员所有订单、用户、服务项目派单、上下架服务、查看统计管理后台权限控制不要只在前端做前端隐藏入口只是体验层面的处理真正的校验必须在后端。常见做法是写一个 HandlerInterceptor在 Controller 执行前把 token 解析成 userId 和 role 放到请求上下文需要管理员权限的接口再单独校验 role而不是每个方法里重复写判断。2.2 为什么用 SSM 而不是 Spring Boot三层架构在养老项目里的职责选题时很多同学会纠结现在新项目不都用 Spring Boot 吗为什么还要用 SSM我的看法是如果你的毕业设计题目明确写了 SSM或者导师指定了这个组合那就按 SSM 来没有必要为了“技术新”去换框架。SSM 三件套在养老服务这种业务逻辑清晰、数据表关系简单的项目里完全够用而且手动配置反而逼你把框架原理搞清楚答辩时被问“Spring 和 SpringMVC 什么关系”不至于答不上来。SSM 三层的职责边界要这样理解Spring 管对象和事务Service 层的类加上 Service 注解交给容器管理涉及多张表更新的方法比如下单同时扣减库存用 Transactional 保证要么全成功要么全回滚。SpringMVC 管路由和参数绑定小程序发来的 HTTP 请求由 DispatcherServlet 分发到对应 ControllerRequestBody 把 JSON 转成 Java 对象RequestMapping 定义 URL 和请求方式。MyBatis 管 SQL它不帮你生成 SQL而是让你把 SQL 写在 XML 或注解里再把查询结果映射成实体对象。养老服务系统的订单查询条件多、组合多MyBatis 的动态 SQL 正好派上用场。这三层不要互相越界。Controller 里只做参数接收和结果封装Service 里写业务规则Mapper 只做数据读写。我见过不少同学的代码在 Controller 里直接 new ServiceImpl或者把 SQL 逻辑写死在 Java 里循环拼接这样的代码答辩时一问一个准。2.3 小程序端到 MySQL 的完整请求链路与统一返回格式明确了职责边界再看一条请求是怎么走通的。用户在服务列表页点“立即预约”小程序通过 wx.request 发起 POST 请求数据是 JSON 格式请求到达 Tomcat 后由 SpringMVC 分发给 OrderControllerController 调用 OrderService 里的 createOrder 方法Service 里先做业务校验再通过 OrderMapper 把订单插入数据库。返回路径是反过来的最终以 JSON 响应交给小程序渲染。这里有一个直接影响开发效率的约定后端所有接口统一返回一个 Result 对象结构固定为三部分code 表示业务状态码200 成功、401 未登录、400 参数错误、500 异常msg 是给前端看的提示文案data 是真正的业务数据。接口一旦统一小程序端就能做一个 request 封装统一处理 loading、错误提示、登录失效跳转不用每个页面重复写一套。public class Result { private int code; private String msg; private Object data; public static Result success(Object data) { Result r new Result(); r.code 200; r.msg success; r.data data; return r; } public static Result error(int code, String msg) { Result r new Result(); r.code code; r.msg msg; return r; } // getter / setter 省略 }code 什么时候用 401 而不是 500要定个规则用户未登录或 token 过期用 401这是前端要主动处理的参数填错用 400前端要给出对应提示系统内部异常用 500前端只弹“系统繁忙”。状态码不统一的话小程序端的 request 封装会越写越乱。注意Result 类放到 common 包下所有 Controller 统一返回它。如果某个接口直接返回实体对象前端拿到的结构一会儿是 {code,msg,data}一会儿是裸对象等于没有约定排查问题时会非常痛苦。3. 后端落地数据表设计、订单接口和 MyBatis 动态 SQL 完整写法业务边界定好、请求链路理清之后后端开发就是照着拆好的模块逐层写。这一章把最核心的三条路径完整走一遍先看五个关键数据表怎么建再看订单创建接口三层怎么写最后看订单列表的多条件查询怎么用 MyBatis 动态 SQL 做。3.1 核心数据表设计用户、服务项目、订单、健康档案、公告数据库设计直接决定后期开发效率表建得不好写 Mapper 的时候会非常痛苦。我对养老系统的建表原则是能用一张表解决的不用两张状态字段用 tinyint 存数字所有时间字段统一用 datetime字符集统一用 utf8mb4。CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL COMMENT MD5或BCrypt加密存储, real_name varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, role tinyint(4) DEFAULT 1 COMMENT 1-家属用户 2-服务人员 3-管理员, address varchar(200) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE service_item ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, type tinyint(4) DEFAULT NULL COMMENT 1-助餐 2-保洁 3-陪医 4-陪伴, price decimal(10,2) DEFAULT NULL, detail text, image_url varchar(200) DEFAULT NULL, status tinyint(4) DEFAULT 1 COMMENT 1-上架 0-下架, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE service_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) DEFAULT NULL COMMENT 业务订单号前端展示用, user_id bigint(20) DEFAULT NULL, item_id bigint(20) DEFAULT NULL, service_time datetime DEFAULT NULL, address varchar(200) DEFAULT NULL, remark varchar(500) DEFAULT NULL, status tinyint(4) DEFAULT 0 COMMENT 0-待接单 1-已接单 2-服务中 3-已完成 4-已取消, staff_id bigint(20) DEFAULT NULL COMMENT 接单服务人员, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE health_record ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) DEFAULT NULL, blood_pressure varchar(20) DEFAULT NULL COMMENT 血压值例如 120/80, heart_rate varchar(20) DEFAULT NULL, record_date date DEFAULT NULL, remark varchar(500) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE notice ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(100) DEFAULT NULL, content text, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字符集这里要说明一下一定要用 utf8mb4 而不是 utf8。MySQL 的 utf8 只支持三个字节的编码家属填个生僻字或者表情符号就会插入失败报错还不直观换成 utf8mb4 一次省心。role 字段用 tinyint 而不是字符串枚举是因为数字在索引和存储上更省空间后端用常量类定义好三个角色值不要散落硬编码在代码里。order_no 订单号不要用自增 id 直接暴露给用户建议在 Service 层生成格式类似“日期时间戳随机数”。status 字段给了 5 个状态状态只允许按 0→1→2→3 的方向推进4 是取消态取消只能在 0 或 1 时发生这个规则放 Service 层校验不要依赖前端传什么就是什么。3.2 服务预约接口Controller → Service → Mapper 三层串起来订单创建是整个系统最核心的接口完整写一遍三层代码。先看 Controller 层RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; PostMapping(/create) public Result create(RequestBody OrderCreateDTO dto, RequestAttribute(userId) Long userId) { // 基础参数校验只做非空判断业务规则放到 Service if (dto.getItemId() null || dto.getServiceTime() null) { return Result.error(400, 服务项目和预约时间不能为空); } ServiceOrder order orderService.createOrder(userId, dto); return Result.success(order); } }dto 是一个只接收参数的类字段就是 itemId、serviceTime、address、remark 这几个。userId 从拦截器解析 token 后塞进 Request 上下文Controller 直接通过 RequestAttribute 取不需要前端再传一个 userId 进来。这一点很多同学会写成前端直接传 userId那别人改个 userId 就能替别人下单了是一个很明显越权漏洞答辩被问概率极高。再看 Service 层Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Override Transactional public ServiceOrder createOrder(Long userId, OrderCreateDTO dto) { // 检验服务项目是否存在且处于上架状态 ServiceItem item orderMapper.findItemById(dto.getItemId()); if (item null || item.getStatus() ! 1) { throw new BizException(服务项目不存在或已下架); } ServiceOrder order new ServiceOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setItemId(dto.getItemId()); order.setServiceTime(dto.getServiceTime()); order.setAddress(dto.getAddress()); order.setRemark(dto.getRemark()); order.setStatus(0); // 新订单默认待接单 order.setCreateTime(new Date()); orderMapper.insert(order); return order; } }这里有两个关键点。第一Transactional 注解必须加虽然当前的 createOrder 只插入了一张表但后续如果要加“下单同时给服务人员发通知”的逻辑事务就能保证通知失败时订单也能回滚不会出现订单建了但通知没发出去的脏数据。第二服务项目必须查一次库并且校验上架状态不能只靠前端传来的 itemId 直接插入订单否则下架的服务还能被下单。Mapper 接口这边就是标准的 MyBatis 写法public interface OrderMapper { int insert(ServiceOrder order); ServiceItem findItemById(Long itemId); ListServiceOrder listOrders(Param(userId) Long userId, Param(status) Integer status, Param(offset) int offset, Param(pageSize) int pageSize); }insert 的 SQL 用 useGeneratedKeys 把自增 id 回填到实体上这样可以省掉查一次数据库拿订单号。findItemById 返回 ServiceItem 而不是拼字符串是为了复用实体类型避免每个方法都建一个中间对象。3.3 MyBatis 多条件查询订单动态 SQL 与分页参数订单列表页要支持按用户查、按状态查、按时间段查组合方式很多。如果每个组合写一条 SQL这接口就没法维护了。MyBatis 动态 SQL 就是为这个场景设计的XML 写法如下select idlistOrders resultTypecom.example.entity.ServiceOrder SELECT * FROM service_order where if testuserId ! null AND user_id #{userId} /if if teststatus ! null AND status #{status} /if if teststartTime ! null AND create_time gt; #{startTime} /if if testendTime ! null AND create_time lt; #{endTime} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /selectwhere 标签会自动处理两个问题如果下面所有 if 都不成立它不会生成 WHERE 关键字整张表按时间倒序返回如果第一个条件成立它会自动去掉开头多余的 AND。这是 MyBatis 里最实用的一个标签写多条件查询时比在 Java 代码里拼接 SQL 字符串干净得多。分页这里没有用 PageHelper 插件而是手动算 offset。原因是在毕业设计里引入插件会多一个配置环节而且 PageHelper 偶尔会出现分页失效的诡异情况排查起来像玄学。手动算 offset 的逻辑很简单第 page 页每页 pageSize 条offset 就是 (page - 1) * pageSize。参数传进来的时候Service 层做一个默认值处理page 默认 1pageSize 默认 10pageSize 上限 50防止有人一次拉走全表。resultType 映射还有一个容易踩的坑如果表字段是下划线命名create_time实体属性是驼峰命名createTime必须在 mybatis-config.xml 里开启驼峰映射否则查出来的对象所有带下划线的字段全是 null。这个配置在避坑章节会专门展开。4. 小程序端实现页面注册、登录态管理与下单全流程后端接口就绪后开始写小程序端。微信小程序的技术栈是 WXML、WXSS、JavaScript本质上是一个被微信限制了 API 的浏览器环境。页面的写法、组件和事件绑定跟 Vue 有些像但又有自己的生命周期和 API 约束比如必须通过 wx.request 发请求、必须用 wx.login 拿登录凭证。4.1 小程序目录结构与 app.json 页面注册顺序一个规范的小程序项目目录是这样的miniprogram/ ├── app.js // 全局逻辑App() 实例 ├── app.json // 全局配置页面注册在这里 ├── app.wxss // 全局样式 ├── pages/ │ ├── index/ // 首页服务列表 │ │ ├── index.wxml │ │ ├── index.js │ │ ├── index.wxss │ │ └── index.json │ ├── detail/ // 服务详情 │ ├── order/ // 下单页 │ ├── orders/ // 我的订单列表 │ ├── profile/ // 个人中心 │ └── login/ // 登录页 └── utils/ └── request.js // 封装 wx.requestapp.json 里的 pages 数组有讲究数组第一个元素就是小程序的启动页正常情况应该放首页每个页面的路径不用写 .wxml 后缀tabBar 里引用的页面必须已经在 pages 数组里注册过而且 tabBar 页面数量不能超过 5 个。{ pages: [ pages/index/index, pages/detail/detail, pages/order/order, pages/orders/orders, pages/profile/profile, pages/login/login ], window: { navigationBarBackgroundColor: #1b8a5a, navigationBarTitleText: 社区养老服务, navigationBarTextStyle: white }, tabBar: { color: #999999, selectedColor: #1b8a5a, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/orders/orders, text: 订单 }, { pagePath: pages/profile/profile, text: 我的 } ] } }tabBar 里的 iconPath 和 selectedIconPath 没有写因为图标文件不存在的话编译会直接报错。如果不想准备图标宁可不配 tabBar改用普通页面加导航跳转也别随便放一个不存在的路径。这是血泪经验项目初始化时最容易在这里翻车。4.2 wx.login 换 token登录态建立与失效处理小程序登录和传统网页登录不一样。小程序不能直接用账号密码登录而是先调用 wx.login 拿一个临时 code再把 code 发给后端后端用 code 去微信接口换 openid。换到 openid 后后端认为用户合法生成一个自定义 token 返回给小程序小程序把 token 存起来后续所有请求带上就行。// utils/request.js const BASE_URL http://localhost:8080; function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method || GET, data: data || {}, header: { content-type: application/json, Authorization: wx.getStorageSync(token) || }, success: (res) { // 业务码 200 才视为成功401 跳登录页 if (res.statusCode 200 res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { wx.navigateTo({ url: /pages/login/login }); reject(res.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }这段代码里最关键的是 401 分支token 失效了不能再调 wx.login 自动续期而是跳登录页让用户重新登录。自动续期的方案也可以做但要在后端加刷新 token 机制复杂度上来了毕业设计没必要。401 跳转登录页的做法简单、可解释答辩时讲得清楚。另一个细节是请求头里的 Authorization 字段后端拦截器从这里取 token前端所有请求都走这个封装就不会存在某个页面忘记带头的问题。登录页拿到 code 后调后端// pages/login/login.js wx.login({ success: (res) { if (!res.code) { wx.showToast({ title: 登录失败, icon: none }); return; } request(/api/auth/login, POST, { code: res.code }) .then((data) { wx.setStorageSync(token, data.token); wx.switchTab({ url: /pages/index/index }); }); } });这里注意一个坑wx.login 取到的 res.code 只能用一次后端 code2session 成功之后这个 code 就作废了。如果同一页面里有多个请求都依赖登录结果一定要先登录取 token再发业务请求不要每个请求里都执行一次 wx.login。4.3 服务列表页与预约下单的衔接首页加载服务列表时在 onLoad 生命周期里调用封装好的 request// pages/index/index.js Page({ data: { serviceList: [] }, onLoad: function () { this.loadServices(); }, loadServices: function () { const self this; request(/api/service/list, GET) .then((list) { self.setData({ serviceList: list }); }); }, goDetail: function (e) { // 取>// pages/order/order.js submitOrder: function () { const { itemId, serviceTime, address, remark } this.data; if (!serviceTime) { wx.showToast({ title: 请选择预约时间, icon: none }); return; } if (!address) { wx.showToast({ title: 请填写服务地址, icon: none }); return; } request(/api/order/create, POST, { itemId: itemId, serviceTime: serviceTime, address: address, remark: remark }).then((order) { wx.redirectTo({ url: /pages/orders/orders?status0 }); }); }下单成功后习惯用 wx.redirectTo 而不是 wx.navigateTo 跳转redirectTo 会关闭当前下单页用户按返回键不会回到已提交的页面再重复提交一次这是个很小但很实用的体验细节。预约时间的组件建议用微信原生 picker 的 modedate 和 modetime 组合不要让用户手输减少格式错误。5. 避坑指南养老系统从开发到答辩的 5 个常见踩坑点这一章是血泪经验合集。下面 5 个坑几乎每次带新人或看同学项目时都会遇到每个都按现象、原因、解决三步写清楚遇到相同症状直接对号入座。5.1 wx.request 请求能通但后端收到的参数全是 null现象后端日志里能看到请求进来了但 Controller 方法参数对象里的字段全是 null或者 RequestBody 的 DTO 直接报“Required request body is missing”。原因小程序端 wx.request 发 POST 时如果 data 传的是普通对象而没有 JSON.stringify并且 header 里没设 content-type 为 application/json后端 RequestBody 就解析不到内容。反过来还有一种情况后端接口用 RequestParam 接收但前端传的是 JSON字段当然也对不上。解决小程序端统一在 request 封装里设置 header 的 content-type 为 application/jsondata 传 JSON.stringify({...})。后端 POST 接口统一用 RequestBody 接对象。两边只要有一边不对排查起来会非常耗时所以约定要在一开始就定好。5.2 MyBatis 查出来的实体字段全是 null现象数据库里明明有数据SQL 在数据库客户端执行也有结果但接口返回的 JSON 里实体字段全是 null尤其是带下划线的字段。原因最常见的是没开启驼峰映射。表字段 user_id 对应实体属性 userId如果 MyBatis 不知道要自动转换映射结果就是 null。另一个原因是实体字段没有生成 getter/setterJSON 序列化时取不到值IDE 不一定会报错。解决在 mybatis-config.xml 里加一行配置settings setting namemapUnderscoreToCamelCase valuetrue/ /settings加了这行之后user_id 会自动映射到 userId。加完还不行就检查实体类是不是只有字段没有 getter用了 Lombok 的 Data 却忘了装插件或者实体字段名和表字段名拼写不一致这类问题 IDE 不会直接报红但运行时就是 null。5.3 wx.login 的 code 被重复使用后端报 code 无效现象用户第一次能登录成功第二次打开小程序或换个页面再登录后端调微信接口换 openid 时返回错误码 40029。原因微信接口的 code 是一次性的用了一次就作废而且有效期只有 5 分钟。很多同学在多个页面的 onLoad 里都调 wx.login或者每次发请求都先 wx.login导致把上一次的 code 又发了一遍。解决只在登录页或 App 启动时调一次 wx.login 拿 code。换到的 token 存到 wx.setStorageSync后续请求从 storage 取 token 用。只有收到 401 时才跳回登录页重新走 wx.login 流程不要在 request 封装里每次自动重新 login。5.4 开发者工具能通真机预览请求直接 fail现象微信开发者工具里接口调得通数据正常但用手机真机预览的时候所有请求全部失败报 request:fail。原因小程序真机环境不会把 localhost 解析成你的电脑。开发者工具默认开了“不校验合法域名”真机预览如果没有同时勾选这个选项或者请求的地址是 localhost就直接被拦住。这是开发环境和小程序线上环境的差异导致的。解决开发阶段在微信开发者工具的详情面板里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。真机调试时把 BASE_URL 里的 localhost 改成电脑的局域网 IP比如 192.168.x.x后端 Tomcat 也要监听 0.0.0.0 而不是 127.0.0.1。要发布上线必须配一个备案过的 HTTPS 域名并在小程序后台加入合法域名列表这一步绕不过去。5.5 数据库时间比实际时间少了 8 小时现象前端展示的订单创建时间比真实时间晚了 8 个小时或者反过来快了 8 个小时而且只有时间部分不对。原因MySQL 驱动连接 URL 里没有指定 serverTimezoneJVM 和数据库服务器时区不一致。MySQL 8.x 默认按 UTC 时间存储中国是 UTC8差了 8 小时。如果 JSON 序列化时 Jackson 没有指定时区时间还会再偏一次。解决JDBC 连接 URL 加上时区参数完整写法是jdbc:mysql://localhost:3306/elder_care?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai同时在 Spring 的 Jackson 配置里设置时间格式yyyy-MM-dd HH:mm:ss和time-zone: GMT8。时间字段在 Java 实体里用 Date 或 LocalDateTime 类型不要用 String 存。这个坑很隐蔽因为它只在特定环境下出现而且日志里不容易看出来属于典型的看着哪里都对但结果不对的问题。6. 答辩前自测清单与一个低成本缓存优化系统跑通之后离答辩还有一段距离。我的习惯是拿出一张纸把每个角色的完整流程手写一遍然后照着走用家属账号下一单、取消一单、查看订单状态变化用服务人员账号接单、更新状态用管理员账号派单、下架一个服务项目、再回来确认小程序端已经看不到下架项目。这三个流程能完整走通系统的主干就稳了。然后加试几条异常路径不登录直接点下单看是否跳登录页把服务项目下架后带着旧链接访问详情页看是否有明确提示断网时点提交按钮看 loading 是否正常关闭。异常路径往往比正常路径更容易暴露赶工痕迹答辩时老师最喜欢挑这些地方问。如果还想做点优化推荐一个低成本方案对服务项目列表这类读多写少的数据做内存缓存。实现方式很简单在 Service 层用一个 ConcurrentHashMap 做缓存定时刷新当管理员上下架服务时主动清掉缓存。代码大致是这样Service public class ServiceItemService { // 读多写少的场景用内存缓存降低数据库压力 private final ConcurrentHashMapLong, ServiceItem cache new ConcurrentHashMap(); private volatile long lastRefresh 0L; private static final long EXPIRE_MS 60 * 1000; public ListServiceItem listOnSale() { refreshIfExpired(); return cache.values().stream() .filter(item - item.getStatus() 1) .collect(Collectors.toList()); } private void refreshIfExpired() { long now System.currentTimeMillis(); if (now - lastRefresh EXPIRE_MS) { ListServiceItem items itemMapper.findAll(); cache.clear(); items.forEach(item - cache.put(item.getId(), item)); lastRefresh now; } } }这个方案的思路是服务项目表数据量小、变更频率低每分钟最多查一次数据库就够了其余请求直接走内存。答辩时讲这个方案一句话就能说清价值读多写少的场景用缓存降低数据库压力。这比硬套一个 Redis 进去更经得起追问因为你可以把缓存原理、失效策略、并发安全讲透而 Redis 如果只是引入却没解决实际问题很容易被问住。我自己的教训是毕业设计做得好的同学不是用了多新潮的技术而是把一条主流程从下单到服务完成做到没有任何逻辑缺口再把异常路径补上最后让系统能讲出自洽的业务故事。把这三件事做完比纠结框架版本重要得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?