简介:SpringBoot社区养老服务系统是一套面向老年服务场景的完整毕业设计资源,基于Spring Boot框架整合Vue前端与MySQL数据库,涵盖用户管理、服务预约、健康档案、费用结算、数据统计等核心模块,既可为养老机构提供业务参考,也为计算机专业学生提供项目实战案例。压缩包共983个文件,以203个Java源码、164个JavaScript、67个Vue组件等前后端代码为主,辅以162个SVG图标、79个GIF动效、69张JPG图片等界面素材,以及SQL数据库脚本、XML配置、启动部署脚本和备份文件;另含论文文档,整体约26.15MB。项目代码结构清晰,包括Spring Security安全控制、前后端分离架构及数据库设计,学习时可按模块研读源码,理解服务逻辑、接口调用与数据表关系,也可直接导入运行调试。已有54人学习浏览,适合毕业设计选题、课程设计或Spring Boot进阶练习。
1. 这个项目到底在解决什么问题:SpringBoot社区养老服务系统的真实定位
社区养老服务系统,说白了就是把社区里老人的基本信息、健康档案、服务工单、护理排班和费用记录统一管起来的那套后台。它不是什么高并发电商项目,真正的技术含量在业务建模和数据表的合理划分上。SpringBoot撑起接口层,数据库负责把老人档案、服务记录、健康数据这些核心业务沉淀下来,论文部分则把系统分析和设计过程写成可交付的文档。这个组合对两类人最有用:一是做Java课程设计和毕业设计的学生,需要一套能讲清楚、能演示、能答辩的技术方案;二是社区信息化服务商,需要一个能快速定制的小型管理后台原型。标题里的“源码数据库和论文”其实就是交付物清单——代码能跑、表结构能导入、文档能说明白设计思路。接下来我就按自己实际做这类项目的顺序,从表结构到核心接口,再到最容易翻车的那些细节,一条条讲清楚。
2. SpringBoot社区养老系统的业务边界与数据库建模:先定表,再写代码
2.1 核心业务模块划分:管理端、服务端与数据流转
拿到这类社区养老需求,第一步不是急着建SpringBoot项目,而是先把业务模块边界划清楚。常见做法是划分成老人档案管理、服务工单管理、健康记录管理、护理人员管理、收费与补贴记录、系统用户与登录权限六大模块。其中老人档案是主数据,服务工单是核心流转数据,健康记录是持续增长的时序数据。
我在设计时会把“老人档案”和“服务工单”作为两条主线:档案管的是“这个老人是谁、住哪、家属联系方式、基础疾病”,工单管的是“谁在什么时间为哪位老人提供了什么服务、状态如何”。健康记录则相对独立,按时间追加,可以后续对接体检设备或护士录入。权限上不需要太复杂的RBAC设计,社区级系统通常两类角色——管理员和护理员,护理员看到自己名下的工单,管理员看全量数据,这已经够了。
模块边界画清楚后,数据库表就呼之欲出。一个SpringBoot项目如果上来就建三十张表,基本是给自己挖坑;保持在十到十五张核心表比较现实。每张表必须能说清“解决什么业务问题”,否则就不该建。
2.2 老人档案与服务工单表结构:字段设计、索引与约束
老人档案表(elder_info)是系统里最重要的主表。我的做法是字段尽量贴合真实管理场景:elder_name、gender、birth_date、id_card、phone、address、emergency_contact、emergency_phone、health_status、blood_type、allergy_history、family_doctor,再加上is_deleted和create_time、update_time这些通用字段。
服务工单表(service_order)则是流转核心。基础字段包括order_no(工单号,业务上要唯一)、elder_id(关联老人)、service_type(上门探视/助餐/保洁/陪同就医)、service_date、start_time、end_time、staff_id(护理员)、status(待接单/进行中/已完成/已取消)、content(服务内容描述)、cost_amount(费用金额)、remark。这里service_date和status是高频查询条件,必须建索引。
CREATE TABLE service_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT '工单号', elder_id BIGINT NOT NULL COMMENT '老人档案ID', service_type TINYINT NOT NULL COMMENT '1-上门探视 2-助餐 3-保洁 4-陪同就医', service_date DATE NOT NULL COMMENT '服务日期', start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, staff_id BIGINT NOT NULL COMMENT '护理员ID', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待接单 1-进行中 2-已完成 3-已取消', content VARCHAR(500) DEFAULT NULL, cost_amount DECIMAL(10,2) DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_elder_date (elder_id, service_date), KEY idx_staff_status (staff_id, status), KEY idx_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='养老服务工单表';这段建表SQL里的几个点值得展开。order_no不要用自增ID代替,因为社区服务后续要对接补贴结算,工单号最好带日期前缀,比如20250612001这种格式,用代码生成而不是数据库自增。elder_id和service_date的组合索引idx_elder_date,直接服务“查某位老人本月所有工单”的高频查询;idx_staff_status服务“查某个护理员待接单列表”,这是护理员端打开App第一个看到的页面。cost_amount用DECIMAL而不是FLOAT,因为涉及补贴金额计算,FLOAT的精度问题在结算时会很难看。
2.3 健康记录与附件的存储策略:JSON字段和文件路径该放哪
健康记录表(health_record)我一般设计成每名老人一行核心基础信息,每次体检或随访新增一条记录。字段包括record_date、record_type(血压/血糖/心率/体温/随访备注)、systolic、diastolic、heart_rate、blood_sugar、temperature、measure_time、operator_id(记录人)、remark。像血压这种带收缩压和舒张压两个关联值的,单独拆成两列而不是存成字符串,这样后续做“筛选收缩压高于160的老人”这类统计时可以直接用SQL比较,不用在代码里切字符串。
这里有一个我踩过的坑:体检报告、服务照片这类附件,不要把文件二进制存进数据库的BLOB字段,也不要试图用数据库管理文件内容。常规方案是在服务器上建一个upload目录,数据库里只存相对路径,上传走SpringBoot的MultipartFile接口,响应里返回URL交给前端拼接。文件一多,数据库只存路径的方式让备份和维护都轻松很多。
健康记录属于持续追加的时序数据,查询pattern基本是“某老人某时间段的记录”,所以索引设计是 (elder_id, measure_time)。这张表日后如果要接体检设备或做趋势分析,可以单独拆成独立的时序存储,但社区级系统用MySQL完全够用,不要一上来就引入时序数据库,那是给后期做的预留。
2.4 逻辑删除与create_time的约定:一张表里必须有,但不是所有表
我在这个项目里统一采用“逻辑删除”策略,所有业务表都带is_deleted字段,删除操作走UPDATE而不是DELETE。原因很实际:社区养老涉及补贴和工单记录,监管审计要追溯,物理删除了就再也找不回来。数据量到几十万行以内,逻辑删除的查询性能损失几乎可以忽略,但换来的是“后悔药”能力。这套机制做下去有个容易翻车的点——唯一索引。比如order_no在业务上应该唯一,但逻辑删除后如果给order_no建了唯一索引,第二次生成相同单号就会冲突。我的做法是order_no由“日期+随机四位”拼出来,重复概率极低,但不用数据库唯一约束,而是在service层查一次确认不存在再插入,牺牲一丁点性能换掉一个坑。
至于create_time和update_time,所有表都加上。update_time记得建表时用ON UPDATE CURRENT_TIMESTAMP,这样框架层更新时不需要手动维护。可以用一条语句验证建的表结构是否合理:SHOW CREATE TABLE service_order;——检查CHARSET是否统一为utf8mb4,排序规则是否一致,否则后面连表JOIN时会出现Illegal mix of collations报错,这是极其典型的翻车现场。
3. SpringBoot服务端分层落地:从老人档案到工单派发,代码怎么写才不返工
3.1 项目结构选择:为什么用Maven多模块,什么时候单模块就够了
SpringBoot项目结构常见两种方案:单模块和多模块。社区养老系统这种规模,单模块完全够,多模块反而增加构建复杂度。我常用的结构:
community-care/ ├── src/main/java/com/community/care/ │ ├── controller/ # 接口层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis数据访问层 │ ├── entity/ # 数据库实体 │ ├── dto/ # 入参/出参对象 │ ├── config/ # 配置类 │ ├── common/ # 通用返回结果、异常处理 │ └── CommunityCareApplication.java ├── src/main/resources/ │ ├── mapper/ # MyBatis XML文件 │ └── application.yml └── pom.xmldto包里的对象不要和entity混用。entity直接映射数据库字段,比如elderInfo里有个字段叫idCard,数据库里是id_card,用MyBatis的下划线自动转驼峰配置搞定。而dto里的ElderSaveDTO用于接收前端入参,ElderVO用于返回给前端展示。很多人图省事直接用entity接收前端参数,短期没问题,一旦数据库字段调整,前端接口也跟着被迫变化,耦合越积越深。分开写虽然多几个类,但维护时不会牵一发动全身。
pom.xml里依赖的版本选择是个高频坑。spring-boot-starter-parent的版本不要随便升,我实际维护时遇到过springboot版本太高导致Druid连接池启动时“discard long time none received connection”的报错刷屏,后来锁定到2.7.x系列配合Druid 1.2.20解决的。版本选择的标准只有一个:稳定组合,不是越新越好。
3.2 老人档案新增与分页查询:Controller、Service、Mapper三段代码的完整逻辑链
先写一个老人档案分页查询接口,这是管理后台最常见的功能。Controller入参是pageNum、pageSize、keyword三个字段,keyword支持模糊匹配姓名或手机号。Service层面要注意分页查询和新增操作的逻辑边界:查询走Mapper的selectPage,新增走insert。
// ElderController.java - 接收前端请求 @RestController @RequestMapping("/api/elder") public class ElderController { @Autowired private ElderService elderService; // 分页查询老人档案,keyword模糊匹配姓名或电话 @GetMapping("/page") public Result<PageResult<ElderVO>> page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { PageResult<ElderVO> result = elderService.pageQuery(pageNum, pageSize, keyword); return Result.success(result); } }Controller层只做参数接收,不写业务逻辑。pageNum默认值为1,pageSize默认值为10,keyword为空时走全量查询,接口分页参数从第1页开始,避免和MyBatis-Plus分页插件“当前页从0开始”的默认行为混淆。
// ElderService.java - 业务逻辑:参数校验、查询、结构化返回 @Service public class ElderService { @Autowired private ElderMapper elderMapper; public PageResult<ElderVO> pageQuery(Integer pageNum, Integer pageSize, String keyword) { // 开启分页,注意PageHelper是线程绑定的,查询完自动失效 PageHelper.startPage(pageNum, pageSize); List<Elder> list = elderMapper.selectPageByKeyword(keyword); // 用PageInfo包装,拿到total总数 PageInfo<Elder> pageInfo = new PageInfo<>(list); List<ElderVO> voList = pageInfo.getList().stream().map(this::toVO).collect(Collectors.toList()); PageResult<ElderVO> result = new PageResult<>(); result.setList(voList); result.setTotal(pageInfo.getTotal()); return result; } }这段代码有两个关键点。PageHelper.startPage()必须在要分页的查询语句之前调用,而且只对紧接着执行的一条查询生效,查询完自动失效。如果startPage之后又执行了别的查询,比如在service里先查了一次权限,再查档案,分页就会失效,总数会错误。另一个是toVO的转换方法,把entity里的idCard等字段直接赋给VO对象,涉及敏感字段比如身份证号时,可以在这里做脱敏处理,比如只返回前6位和后4位,中间用星号补齐。
<!-- ElderMapper.xml - 分页查询SQL --> <select id="selectPageByKeyword" resultType="com.community.care.entity.Elder"> SELECT id, elder_name, gender, birth_date, phone, address, emergency_contact, health_status, is_deleted, create_time FROM elder_info <where> is_deleted = 0 <if test="keyword != null and keyword != ''"> AND (elder_name LIKE CONCAT('%', #{keyword}, '%') OR phone LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY create_time DESC </select>这里selectPageByKeyword的返回类型用了entity,但只查需要的列而不是SELECT *。LIKE查询的keyword默认不带%,用CONCAT函数拼上,避免SQL注入的一个常见方式。WHERE里面强制is_deleted = 0,保证逻辑删除的数据永远不会因为漏了条件被查出来。很多框架自动生成的Mapper.xml不带这个条件,你需要在每个业务查询里主动补上,这是逻辑删除方案里最容易漏的一环。
3.3 服务工单状态流转与事务边界:@Transactional用的时机和误用
工单派发是社区养老系统的核心业务:管理员创建工单,派给某个护理员,护理员接单,服务完成后标记完成。状态流转我选择把所有操作收敛到service层,不在Controller里直接改状态字段。
工单创建要考虑事务。创建工单的同时要更新老人的累计服务次数或关联的补贴记录,这两个操作必须在一个事务里——要么同时成功,要么同时回滚。这里@Transactional注解必须加在public方法上,而且只能通过Spring代理调用才生效。最常见的翻车场景是同类内部调用,一个方法调同类另一个加了@Transactional的方法,事务不生效,因为走的是this调用而不是Spring代理。另一个常见问题是事务方法里catch了异常并吞掉,导致事务无法感知失败,数据半成功半失败。
// WorkOrderService.java - 工单派发核心逻辑 @Service public class WorkOrderService { @Autowired private ServiceOrderMapper orderMapper; @Autowired private ElderService elderService; // 创建工单并更新老人最近服务时间,两次写操作需要事务保护 @Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateDTO dto) { // 1. 校验老人存在且未被删除 Elder elder = elderService.getById(dto.getElderId()); if (elder == null ) { throw new BizException("老人档案不存在或已注销"); } // 2. 生成业务工单号:日期前缀 + UUID后四位,保证可读性 ServiceOrder order = new ServiceOrder(); order.setOrderNo(generateOrderNo()); order.setElderId(dto.getElderId()); order.setServiceType(dto.getServiceType()); order.setServiceDate(dto.getServiceDate()); order.setStaffId(dto.getStaffId()); order.setStatus(0); order.setContent(dto.getContent()); order.setCostAmount(dto.getCostAmount()); orderMapper.insert(order); // 3. 更新老人最近服务时间,用于管理端展示活跃度 elderService.updateLastServiceTime(dto.getElderId(), dto.getServiceDate()); return order.getId(); } }rollbackFor = Exception.class这个参数很关键。默认情况下Spring事务只对RuntimeException回滚,如果你在业务代码里抛的是自定义BizException,而BizException继承了Exception而不是RuntimeException,事务就不会回滚。我的习惯是自定义业务异常统一继承RuntimeException,然后加上rollbackFor = Exception.class,双保险。generateOrderNo的规则我用的是年月日时分秒加三位随机数,比如20250612153000123,三十位以内,MySQL的VARCHAR(32)刚好够用。
3.4 MyBatis-Plus还是MyBatis:选型思考和一个必踩的坑
社区养老系统这种简单CRUD占主体的项目,MyBatis-Plus确实能省不少样板代码,内置的BaseMapper提供了insert、selectById、updateById等方法,分页插件也直接集成。我的建议是:项目初期用MyBatis-Plus写增删改查,复杂报表查询和自定义JOIN用XML写。两者同时存在没有冲突,这在SpringBoot整合里是常规操作。
用MyBatis-Plus最典型的坑在于逻辑删除的全局配置。MyBatis-Plus的@TableLogic注解支持逻辑删除配置,配置后框架自动帮你把deleteById转成UPDATE is_deleted=1。但如果你同时手写了UPDATE语句,这个注解不会生效,必须自己在SQL里补条件。
# application.yml 中的关键配置 mybatis-plus: global-config: db-config: logic-delete-field: isDeleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置解决的就是数据库id_card和Java字段idCard的映射问题。如果你用MyBatis而不是MyBatis-Plus,需要在mybatis-config.xml或者application.yml里同样开启。没开这个配置,查询结果里idCard永远是null,这是新手最常见的“查出来全是空”的原因。
另一个MyBatis-Plus的常见问题是字段自动填充。createTime和updateTime想自动写入,可以实现MetaObjectHandler接口,在insertFill和updateFill方法里统一设置。但要注意,如果数据库里已经设置了DEFAULT CURRENT_TIMESTAMP,代码层再赋值会职责重叠。我的做法是加MySQL层的默认值兜底,同时代码层不重复设置,两套机制只选一套,避免某次更新时updateTime没有刷新。
4. 开发与部署阶段高频排查目录:SpringBoot+MySQL的八个必踩和后悔药配方
4.1 时区问题引发的两个小时定位:连接串里必须带的参数
SpringBoot项目连MySQL,最经常出现的诡异现象是:数据库里存的时间比北京时间晚了8小时,或者查询出来的时间同样晚8小时。根本原因是MySQL连接时区设置和服务器时区不一致。我的连接串里必须完整写上serverTimezone=Asia/Shanghai,写成这样:
jdbc:mysql://localhost:3306/community_care?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false这个连接串里useSSL=false是因为本地开发环境一般没有配置SSL证书,不关掉会报SSL连接警告甚至握手失败。characterEncoding=utf8要显式声明,否则中文写入会出现乱码,虽然MySQL 8.0默认utf8mb4可以省略,但写上更保险。
如果项目用Druid连接池,还要在Druid的配置里也加上相同时区参数。SpringBoot的url通过配置项spring.datasource.url注入,Druid的连接参数则通过spring.datasource.druid.connection-properties补充。两边不一致时,时区仍然可能出问题。排查这类问题最快的方式:登录MySQL命令行,执行SELECT NOW(),看数据库当前时间和系统时间是否一致,不一致就在MySQL配置文件里加上default-time-zone='+08:00'。
4.2 前端Vue打包放进SpringBoot的路径坑:静态资源和接口路由冲突的解决
这个项目的交付形态通常是Vue前端打包后放进SpringBoot的static目录,变成一个可执行JAR直接部署。这个方案本身没问题,但路由冲突时有发生。前端的/index.html和SpringBoot的@RestController路径只要有一个重叠,页面就会返回JSON而不是页面内容。
我的做法是在application.yml里配置Spring MVC的路径前缀:
spring: mvc: pathmatch: matching-strategy: ant_path_matcher resources: static-locations: classpath:/static/前端打包后的dist目录内容直接复制到src/main/resources/static/下,然后通过http://localhost:8080/index.html访问。如果前端用了history模式路由,需要配置一个转发规则,把非API的路径转发到index.html,否则用户刷新页面时会出现404。社区养老系统这类管理后台建议直接用hash模式路由,地址栏带#号,省掉配置转发的麻烦,刷新也不会出错。
还有一个必须处理的问题是API路径统一加/api前缀,Controller的RequestMapping统一写/api/...,这样静态资源和API接口在物理上就分开了,前端开发时期通过nginx代理解决跨域,生产阶段同源部署,开发环境和生产环境的行为保持近似一致,能少很多排查成本。
4.3 SpringBoot版本太高引发的连锁问题:Druid、FastJSON与JDK版本不匹配
标题相关热搜词里有个“springboot版本太高”,这确实是很多维护者的切肤之痛。SpringBoot 3.x要求JDK 17起步,早期版本的Druid、FastJSON、MyBatis-Plus(旧版)都跑不起来,报错通常是NoSuchMethodError或者ClassNotFoundException。而社区养老系统这种项目一般都基于JDK 8在跑,我的经验是不要把SpringBoot堆到3.x,固定在2.7.x系列,MySQL驱动用8.0.33,Druid用1.2.20,MyBatis-Plus用3.5.3.x,这套组合能稳定兼容,后端升级换代的动力是业务需求,而不是版本焦虑。
版本升级时还要检查MyBatis-Plus的分页插件配置。新版把PaginationInnerInterceptor的dbType参数从DbType.MYSQL变成了可选,配置不对时分页查询会返回全量数据,这个很难发现,需要打印SQL日志对比。排查办法是设置mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl,在控制台看到limit关键字,有就是分页生效,没有就是没生效。
4.4 数据库脚本交付的三件套:建库、建表、初始化数据,少一个都跑不起来
交付源码和数据库时,最让我头疼的是脚本不完整。一个能跑的数据库脚本必须包含三块:create database、use database、建表语句和初始化数据。很多人只给了建表语句,导入时直接报No database selected。我现在的做法是单独建一个sql目录,放init_db.sql和init_data.sql两个文件,init_db.sql里写清楚数据库名和字符集,init_data.sql里放管理员账号、测试老人档案、护理员等初始化数据,管理后台才能登录得进去。
-- init_db.sql - 初始化数据库 CREATE DATABASE IF NOT EXISTS community_care DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE community_care;这里注意utf8mb4_general_ci是排序规则,中文和英文字符都能正确排序。如果用了utf8mb4_unicode_ci,在某些MySQL版本下LIKE查询中文可能异常。字符集和排序规则统一是防止Illegal mix of collations报错最有效的办法。初始化管理员密码建议用MD5加密后写入,比如“admin123”的MD5值,SpringBoot的登录模块用DigestUtils.md5DigestAsHex做同样处理,两边才比对得上。
4.5 一个隐藏的深坑:@TableLogic和唯一索引的冲突
这个问题在真实项目里困扰了我一周。表里用is_deleted做逻辑删除,同时又给phone字段建了唯一索引,插入第二个相同手机号的老人时,数据库直接报Duplicate entry。逻辑删除只是标记,数据还在表里,唯一索引冲突无法避免。
解决方案常见两个:一个是不建唯一索引,通过service层查询判断是否已存在;另一个是给表额外加一个deleted_unique标识,把is_deleted参与唯一索引。比如建唯一索引UNIQUE KEY uk_phone_deleted (phone, is_deleted),这样同一个人第二次插入时is_deleted=0,还是冲突。更靠谱一点的做法是采用“软删除+业务主键重新生成”的策略,比如同一手机号重新建档时,把老记录物理删除或改个手机号末尾标记。社区养老场景我一般直接去掉phone的唯一索引,因为实际业务中老人电话变更并不罕见,重复可能性不高,靠service层判断就够了。
4.6 常见错误信息自查表:控制台报错时先看这三条
整理一份我自己的报错自查顺序,新手排查效率能快很多:
| 报错关键词 | 可能原因 | 解决动作 |
|---|---|---|
| Access denied for user | 数据库账号密码错误或权限不足 | 检查application.yml中的用户名密码,用root账号测试连接 |
| Table doesn't exist | 表未创建或库名不对 | 确认use了正确的数据库,执行SHOW TABLES查看表清单 |
| Unknown column 'xxx' | 表结构和实体字段不匹配 | 对比entity字段和表结构,检查驼峰映射是否开启 |
| Packet for query is too large | MySQL单次查询包大小限制 | 修改max_allowed_packet参数,或缩小查询范围 |
| Connection timed out | 数据库地址不通或防火墙拦截 | ping数据库地址,telnet 3306端口确认 |
其中Packet for query is too large通常出现在一次性传入体检报告长文本或批量插入大量数据时,报错突然出现又不太容易排查,修改MySQL的max_allowed_packet就能解决,但这属于环境参数问题,不是代码bug。
从实际维护的角度说,这些坑每一条都要自己踩过一次才有印象。时区问题我压了两小时,版本问题耗了大半天,参数配置问题更是反复排查。建议新项目开工前,先把第4章这些配置一字不差地写进预检清单,能保你少通宵几个晚上。数据库SQL社区版和企业版差异不大,但MySQL 8.0以上用utf8mb4是底线,连接串、建表语句、字符集三处统一,这是数据库同步到任何环境都能跑的前提。
5. 性能优化与数据验证技巧:用Explain、慢查询日志让系统“抗造”
项目跑通只是第一步,如何让这个系统在真实社区场景下不至于越用越卡,需要几项实用优化。第一个动作是给高频查询的报表统计加缓存中间层,但这个项目数据量不会太大,Redis缓存实际上不需要急于引入,反倒是索引设计和SQL写法影响更大。
最值得掌握的技巧是EXPLAIN查看SQL执行计划。在Navicat或者命令行执行EXPLAIN SELECT * FROM service_order WHERE elder_id = 100 AND service_date >= '2025-01-01';,确认type不是ALL(全表扫描),possible_keys和key里出现了idx_elder_date,rows估算值在合理范围。如果索引没生效,先检查SQL里的字段有没有隐式类型转换——比如elder_id在Java里是Long,在数据库里是BIGINT,关联条件两边类型一致才行。最常见的不生效场景是查询条件里对索引列加了函数,WHERE DATE(create_time) = '2025-06-01',这个写法让索引完全失效,改成范围查询create_time >= '2025-06-01' AND create_time < '2025-06-02'才能走索引。
第二项优化是全局开启慢查询日志。在MySQL配置文件的mysqld分段加上:
slow_query_log = ON slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1log_output默认是FILE,可以改成TABLE方便查询。部署上线后每三天看一次慢查询日志,把执行时间超过1秒的SQL捞出来,逐个分析索引。社区养老系统的数据量通常到不了需要读写分离的程度,单库单表加合理索引足以撑住几千名老人的常规访问,用不着引入分库分表这些重量级方案。
第三项是统计类SQL的写法优化。比如统计本月各服务中心工单量:SELECT service_type, COUNT(*) FROM service_order WHERE service_date BETWEEN '2025-06-01' AND '2025-06-30' GROUP BY service_type,这语句需要索引覆盖service_date列。但如果同时要关联elder_info表取区域字段,关联表的驱动顺序会影响性能。我建议优先用单表统计,再在业务Service层做Java内存聚合,因为社区级数据量小,内存聚合完全够用,还能避免复杂JOIN带来的索引走向不可控问题。
第四项值得讲的是工单历史数据的归档策略。服务工单表数据增长最快,一年后可能积累几十万行。我的做法是维护一个archive线程,按季度把状态为“已完成”且超过半年的工单迁移到service_order_history表,业务查询默认只查主表,需要历史数据走专门接口。这个方案比用存储过程简单可控,透明地板到SpringBoot的定时任务里。
在实际做这类系统的成长路径中,我的习惯是每完成一个模块就写一份简短的接口与表结构对应清单,比如“工单状态流转涉及service_order表的status字段和service_order_log表的操作记录”。这样交付论文时,论文里的系统设计部分直接从清单里组织素材,不赶工,质量也有保证。如果你在这个基础上继续加功能,建议优先做统计报表和导出Excel,这两个功能对实际使用者有直观价值,对答辩演示也有亮点。做完这些再回头看标题里的“源码数据库和论文”,其实每一项都得能拿得出手——代码能跑是底线,数据库能导是基本,文档能对上才是加分的部分。希望这篇笔记能帮你在做这套系统时少走一段弯路,哪怕省下半天排查时间,也算值了。
本文还有配套的精品资源,点击获取