ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot养老院管理系统全栈开发实战:从业务设计到部署运维

SpringBoot养老院管理系统全栈开发实战:从业务设计到部署运维 1. 项目概述与核心价值最近在整理过往的毕业设计和商业项目时翻到了一个挺有意思的案例一个基于SpringBoot的养老院管理系统。项目编号是36270听起来很学术但背后涉及的业务逻辑和技术选型其实对很多想入行企业级应用开发的朋友尤其是面临毕业设计的同学非常有参考价值。这个系统本质上是一个针对养老机构日常运营的综合性管理平台它要解决的痛点非常具体如何将老人档案、护理计划、床位管理、费用结算、员工排班这些琐碎又关键的事务从纸质表格和Excel的汪洋大海里解放出来实现数字化、流程化的高效管理。我之所以觉得这个项目值得拿出来细说是因为它麻雀虽小五脏俱全。它不像一些纯炫技的Demo只关注某个酷炫的技术点相反它完整地覆盖了一个典型管理系统的核心模块并且业务逻辑有很强的现实映射。对于学习者而言你能从中看到如何将具体的业务需求比如“为老人制定一周的护理计划”转化为数据库表设计、后端接口和前端的交互流程。从技术栈来看“SpringBoot”这个前缀也很有代表性它几乎成了当前Java后端开发的事实标准搭配上主流的前端框架和数据库构成了一个非常经典且实用的技术组合。无论你是想理解一个完整项目的开发脉络还是为自己的毕业设计寻找灵感和可复用的代码结构这个项目都能提供一个清晰的蓝本。2. 系统整体设计与架构思路拆解2.1 核心业务模块解析一个养老院管理系统其核心是围绕“人”老人、员工、“事”护理、活动、“物”床位、物资、“财”费用、账单这四大维度展开的。在项目36270中我将其主要业务模块拆解如下老人信息管理这是系统的基石。不仅仅是记录姓名、年龄、身份证号等基本信息更重要的是健康档案包括既往病史、药物过敏史、当前用药、体检报告等。这部分设计需要考虑到数据的隐私性和安全性同时要支持快速检索和更新。床位与入住管理模拟养老院的物理空间。需要管理楼栋、楼层、房间、床位的信息并实现床位的状态管理空闲、已入住、维修中。入住管理则是一个完整的流程从预约登记、合同签订、分配床位到正式入住涉及多张业务表的联动。护理服务管理这是体现专业性的模块。系统需要支持制定个性化的护理计划如每日测量血压、每周洗澡次数、特殊饮食要求并记录每次护理的执行情况由哪位护工在何时完成老人状态如何。这通常需要一套可配置的护理项目库。员工与排班管理管理护工、护士、医生、后勤等各类员工信息。核心难点在于智能排班需要综合考虑员工的工种、技能等级、工作时长限制以及老人的护理需求生成合理且公平的班表。费用与财务管理系统需要自动计算每位老人每月产生的费用包括床位费、护理费、餐费、医药费等。费用项目可能因人而异且可能有政府补贴或保险抵扣等复杂情况。生成账单、记录缴费情况、提供财务报表是基本功能。库存与物资管理管理养老院的日常消耗品如药品、食品、护理用品等。包括采购入库、领用出库、库存盘点、效期预警等功能确保物资供应不断档同时控制成本。家属端与通知模块现代养老系统越来越注重家属的参与。通常需要一个家属端可能是小程序或H5页面让家属能查看老人的日常动态经授权、护理记录、费用明细并能在线沟通或预约探视。2.2 技术架构选型与考量为什么选择“SpringBoot”作为技术底座这是基于快速开发、易于维护和生态成熟度的综合考量。后端SpringBoot MyBatis-Plus MySQLSpringBoot它极大地简化了Spring应用的初始搭建和开发过程通过自动配置和起步依赖让我们能快速构建出独立运行、生产级别的应用。对于毕业设计或中小型项目能省去大量繁琐的XML配置专注于业务逻辑。MyBatis-Plus这是在经典ORM框架MyBatis之上的强大增强工具。它提供了通用的Mapper和Service封装单表CRUD操作几乎不用写SQL极大地提升了开发效率。同时它保留了MyBatis灵活编写复杂SQL的能力在需要多表关联查询或复杂统计时游刃有余。对于管理系统大量表单操作的特点MyBatis-Plus非常合适。MySQL成熟、稳定、开源的关系型数据库。养老院系统的数据关系明确老人、床位、员工等实体间关系清晰事务性要求强如缴费、入库操作选择MySQL是稳妥且主流的选择。考虑到数据量初期单库单表完全足够后期若有性能压力再考虑分库分表。前端主流框架三选一Vue/React/Angular Element UI/Ant Design项目源码36270可能采用了其中一种组合。目前国内Vue.js Element UI的搭配非常流行因其学习曲线平缓、组件丰富且符合后台管理系统开发习惯。前后端分离这是现代Web开发的标配。后端通过RESTful API提供数据接口前端独立部署通过Ajax调用接口。这样做的好处是前后端开发可以并行职责清晰且后端API可以同时服务于Web管理端和未来的移动端如家属小程序。其他关键技术点权限控制Spring Security / Shiro管理系统必有权限区分。院长、护士长、普通护工、财务人员能看到和操作的功能截然不同。通常会采用基于角色的访问控制RBAC模型实现菜单权限和按钮级权限的控制。日志与监控使用SLF4J Logback记录操作日志和系统日志便于审计和问题排查。可集成Spring Boot Actuator进行简单的应用健康监控。缓存Redis对于频繁访问且变化不大的数据如字典项、部门信息可以引入Redis作为缓存减轻数据库压力提升响应速度。实操心得在技术选型上切忌为了“炫技”而堆砌新技术。对于毕业设计或初创项目“成熟、稳定、社区活跃”是比“新颖、前沿”更重要的标准。SpringBoot MyBatis-Plus Vue这一套资料丰富遇到问题几乎都能找到解决方案能让你把更多精力花在业务逻辑的实现上而不是折腾环境或解决冷门框架的Bug。3. 核心模块详细设计与实现要点3.1 数据库设计表结构背后的业务逻辑数据库设计是系统的骨架设计得好后续开发事半功倍。我们以几个核心表为例看看业务逻辑是如何体现在表结构中的。1. 老人表 (elderly)这张表不仅要存基本信息更要为后续的护理、费用模块预留关联。CREATE TABLE elderly ( id bigint(20) NOT NULL COMMENT 主键ID, name varchar(50) NOT NULL COMMENT 姓名, id_card varchar(18) UNIQUE COMMENT 身份证号, gender tinyint(1) DEFAULT NULL COMMENT 性别0女1男, birth_date date DEFAULT NULL COMMENT 出生日期, contact_phone varchar(20) COMMENT 联系电话, emergency_contact varchar(50) COMMENT 紧急联系人, emergency_phone varchar(20) COMMENT 紧急联系电话, health_status varchar(500) COMMENT 健康状况简述, checkin_date date DEFAULT NULL COMMENT 入住日期, bed_id bigint(20) DEFAULT NULL COMMENT 当前床位ID, status tinyint(1) DEFAULT 1 COMMENT 状态1在住0已退住, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人信息表;关键字段解析bed_id外键关联床位表确立了老人与床位的归属关系。status字段用于软删除或状态流转避免物理删除数据丢失历史记录。2. 护理计划与记录表 (care_plan,care_record)这是业务核心设计上常采用“计划”与“执行记录”分开的模式。care_plan护理计划表存储为某个老人制定的周期性护理安排如plan_name计划名称elderly_id老人IDcycle_type周期类型每日/每周items护理项目JSON数组如[“测血压” “喂药”]start_date,end_date。care_record护理记录表每次执行护理后生成一条记录。字段包括plan_id关联的计划IDelderly_idnurse_id护工IDcare_item具体项目execute_time执行时间result_notes执行结果备注signature护工电子签名或确认标识。注意事项护理项目 (items) 使用JSON格式存储在数据库中虽然查询效率不如关系表但极大增加了灵活性。护理项目可以动态增删而无需频繁修改表结构。这是一种在灵活性与范式之间的权衡在类似的可配置项场景中很常见。如果后期需要基于护理项目进行复杂统计则建议还是拆分成独立的care_item表和关联表。3. 费用账单表 (fee_bill)费用计算是财务模块的难点关键在于设计出能容纳各种费用类型和复杂计算逻辑的结构。CREATE TABLE fee_bill ( id bigint(20) NOT NULL, bill_no varchar(30) NOT NULL COMMENT 账单编号规则生成如202405-1001, elderly_id bigint(20) NOT NULL COMMENT 老人ID, bill_month date NOT NULL COMMENT 账单月份如2024-05-01, total_amount decimal(10,2) NOT NULL COMMENT 总金额, discount_amount decimal(10,2) DEFAULT 0.00 COMMENT 优惠金额, actual_amount decimal(10,2) NOT NULL COMMENT 实际应付金额, payment_status tinyint(1) DEFAULT 0 COMMENT 支付状态0未付1部分付2已付清, generated_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 生成时间, PRIMARY KEY (id), UNIQUE KEY uk_bill (elderly_id, bill_month) -- 防止重复生成月度账单 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT费用账单主表;同时需要一张账单明细表 (fee_bill_detail) 来记录构成总金额的每一笔费用bill_id账单IDfee_item费用项目如床位费unit_price单价quantity数量subtotal小计remark备注如“三楼单人间”。设计思路这种主表-明细表的结构清晰地反映了业务关系一张账单包含多个费用项也便于后续对账和查询。bill_month字段用于按月份生成和汇总账单。payment_status的枚举值设计覆盖了常见的支付场景。3.2 后端核心业务逻辑实现以“生成月度费用账单”这个核心服务为例我们来看后端代码如何组织。1. 服务层接口设计首先在Service层定义清晰的接口public interface FeeBillService { /** * 为指定老人生成某月的费用账单 * param elderlyId 老人ID * param month 月份格式 yyyy-MM * return 生成的账单ID */ Long generateMonthlyBill(Long elderlyId, String month); /** * 批量生成所有在住老人的月度账单通常在月初定时任务中调用 * param month 月份 */ void generateAllBillsForMonth(String month); }2. 服务实现逻辑拆解generateMonthlyBill方法的实现是业务逻辑的集中体现Service Slf4j public class FeeBillServiceImpl extends ServiceImplFeeBillMapper, FeeBill implements FeeBillService { Autowired private ElderlyService elderlyService; Autowired private FeeItemConfigService feeItemConfigService; // 费用项配置服务 Autowired private BedService bedService; Transactional(rollbackFor Exception.class) // 关键添加事务管理 Override public Long generateMonthlyBill(Long elderlyId, String month) { // 1. 参数校验与数据准备 if (elderlyId null || StringUtils.isBlank(month)) { throw new BusinessException(参数错误); } Elderly elderly elderlyService.getById(elderlyId); if (elderly null || elderly.getStatus() ! 1) { throw new BusinessException(老人不存在或非在住状态); } // 检查是否已生成过该月账单 if (this.isBillExists(elderlyId, month)) { throw new BusinessException(month 账单已存在); } // 2. 计算各项费用 ListFeeBillDetail detailList new ArrayList(); BigDecimal totalAmount BigDecimal.ZERO; // 2.1 计算床位费根据老人当前床位类型和价格 Bed currentBed bedService.getById(elderly.getBedId()); if (currentBed ! null) { BigDecimal bedFee currentBed.getMonthlyPrice(); // 假设床位表有月租价格字段 detailList.add(createDetail(床位费, bedFee, 1, bedFee, currentBed.getRoomNumber())); totalAmount totalAmount.add(bedFee); } // 2.2 计算护理费根据护理计划中的项目汇总 BigDecimal careFee calculateCareFee(elderlyId, month); detailList.add(createDetail(护理费, careFee, 1, careFee, month 护理服务汇总)); totalAmount totalAmount.add(careFee); // 2.3 计算其他固定或动态费用如餐费、医药费可从其他表查询 // ... 此处省略其他费用计算逻辑 // 3. 应用优惠或补贴例如政府补贴、机构内部优惠 BigDecimal discount calculateDiscount(elderlyId, totalAmount); BigDecimal actualAmount totalAmount.subtract(discount); // 4. 创建账单主记录和明细记录 FeeBill bill new FeeBill(); bill.setBillNo(generateBillNo(elderlyId, month)); // 生成唯一账单号 bill.setElderlyId(elderlyId); bill.setBillMonth(DateUtil.parse(month -01)); // 月份转日期 bill.setTotalAmount(totalAmount); bill.setDiscountAmount(discount); bill.setActualAmount(actualAmount); bill.setPaymentStatus(0); this.save(bill); // 保存主表 // 保存明细列表并关联主表ID for (FeeBillDetail detail : detailList) { detail.setBillId(bill.getId()); } feeBillDetailService.saveBatch(detailList); // 批量保存明细 log.info(成功生成账单老人ID{}, 月份{}, 账单ID{}, elderlyId, month, bill.getId()); return bill.getId(); } // 辅助方法创建明细对象 private FeeBillDetail createDetail(String item, BigDecimal unitPrice, Integer quantity, BigDecimal subtotal, String remark) { FeeBillDetail detail new FeeBillDetail(); detail.setFeeItem(item); detail.setUnitPrice(unitPrice); detail.setQuantity(quantity); detail.setSubtotal(subtotal); detail.setRemark(remark); return detail; } // 辅助方法检查账单是否存在 private boolean isBillExists(Long elderlyId, String month) { QueryWrapperFeeBill wrapper new QueryWrapper(); wrapper.eq(elderly_id, elderlyId) .eq(bill_month, DateUtil.parse(month -01)); return this.count(wrapper) 0; } }代码逻辑解读事务管理Transactional注解确保账单主表和所有明细表要么一起保存成功要么一起失败回滚保证数据一致性。防御性编程在开始核心计算前进行充分的参数校验和业务状态检查如老人是否在住、账单是否已存在避免产生脏数据。模块化计算将床位费、护理费、其他费用的计算拆分成独立的方法或调用其他服务保持主方法清晰。calculateCareFee方法内部会去查询该老人当月的护理记录并按项目单价汇总。明细驱动先计算并构建所有费用明细 (detailList)再汇总出总金额。这样生成的账单有据可查便于后期核对。日志记录在关键操作点记录日志便于问题追踪和审计。3.3 前端页面交互与组件化实践前端采用Vue Element UI重点在于构建清晰、易用的管理界面。以“老人信息管理”页面为例。1. 页面布局与组件结构通常采用经典的“上查询中按钮下表格”布局。template div classelderly-management !-- 顶部查询区域 -- el-card shadownever classsearch-box el-form :inlinetrue :modelqueryParams el-form-item label姓名 el-input v-modelqueryParams.name placeholder请输入姓名 clearable / /el-form-item el-form-item label状态 el-select v-modelqueryParams.status placeholder请选择 el-option label全部 value / el-option label在住 :value1 / el-option label已退住 :value0 / /el-select /el-form-item el-form-item el-button typeprimary clickhandleQuery查询/el-button el-button clickresetQuery重置/el-button /el-form-item /el-form /el-card !-- 中部操作按钮区域 -- el-card shadownever classoperation-box el-button typesuccess iconel-icon-plus clickhandleAdd新增老人/el-button el-button typewarning iconel-icon-download clickhandleExport导出/el-button !-- 其他批量操作按钮 -- /el-card !-- 底部数据表格区域 -- el-card shadownever el-table :datatableData v-loadingloading border stylewidth: 100% el-table-column propid labelID width80 aligncenter / el-table-column propname label姓名 width120 / el-table-column propgender label性别 width80 aligncenter template #defaultscope {{ scope.row.gender 1 ? 男 : 女 }} /template /el-table-column el-table-column propidCard label身份证号 width180 / el-table-column propbedInfo label床位 width150 / el-table-column propstatus label状态 width100 aligncenter template #defaultscope el-tag :typescope.row.status 1 ? success : info {{ scope.row.status 1 ? 在住 : 已退住 }} /el-tag /template /el-table-column el-table-column propcheckinDate label入住日期 width120 / el-table-column label操作 width220 aligncenter fixedright template #defaultscope el-button sizemini typetext clickhandleView(scope.row)查看/el-button el-button sizemini typetext clickhandleEdit(scope.row)编辑/el-button el-button sizemini typetext clickhandleCheckout(scope.row) :disabledscope.row.status ! 1办理退住/el-button /template /el-table-column /el-table !-- 分页组件 -- el-pagination size-changehandleSizeChange current-changehandleCurrentChange :current-pagequeryParams.pageNum :page-sizes[10, 20, 50] :page-sizequeryParams.pageSize layouttotal, sizes, prev, pager, next, jumper :totaltotal classpagination-container /el-pagination /el-card !-- 新增/编辑对话框组件 -- elderly-form-dialog refformDialog successfetchData / /div /template2. 关键交互逻辑数据获取在mounted生命周期或查询按钮点击时调用fetchData方法将queryParams包含页码、页数、查询条件发送给后端API。组件化将“新增/编辑”表单封装成独立的ElderlyFormDialog组件。通过ref调用其open方法并通过success事件监听表单提交成功刷新表格数据。这符合Vue的组件化思想提高了代码复用性和可维护性。状态控制操作按钮根据行数据状态动态禁用。例如只有状态为“在住”的老人才能点击“办理退住”按钮。分页处理Element UI的分页组件与后端分页查询完美配合。handleSizeChange和handleCurrentChange方法负责更新查询参数并重新获取数据。实操心得前端开发中合理拆分组件至关重要。将表单、弹窗、复杂表格单元格等封装成独立组件能使父页面逻辑更清晰。同时善用Element UI提供的v-loading、el-tag、el-tooltip等组件能极大提升用户体验和界面美观度。对于表格中复杂的操作列使用template #defaultscope可以灵活自定义渲染内容。4. 系统部署与运维关键点4.1 后端应用打包与部署SpringBoot应用部署非常简便主要分为以下几步打包在项目根目录使用Maven命令mvn clean package -DskipTests进行打包。这会生成一个可执行的JAR文件如elderly-management-0.0.1-SNAPSHOT.jar该JAR文件内嵌了Tomcat服务器无需额外安装Web容器。配置文件分离这是生产环境部署的最佳实践。不要在打包的JAR里写死数据库连接等配置。而是使用application-prod.yml或application-prod.properties文件并通过启动参数指定激活该配置文件。java -jar elderly-management-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod将application-prod.yml放在JAR包同级目录或指定路径下SpringBoot会自动读取。这样不同环境开发、测试、生产的配置可以完全隔离。使用启动脚本创建一个Shell脚本如startup.sh来管理应用可以方便地设置JVM参数、日志路径等。#!/bin/bash APP_NAMEelderly-management-0.0.1-SNAPSHOT.jar LOG_PATH./logs/elderly-management.log JVM_OPTS-Xms512m -Xmx1024m -XX:UseG1GC nohup java $JVM_OPTS -jar $APP_NAME --spring.profiles.activeprod $LOG_PATH 21 echo Application $APP_NAME is starting...运行sh startup.sh即可后台启动应用。nohup和确保终端关闭后应用仍在运行。4.2 前端项目构建与部署Vue项目需要先构建生成静态资源文件。构建在Vue项目目录下运行npm run build或yarn build。这会在项目下生成一个dist文件夹里面是压缩优化后的HTML、CSS、JavaScript文件。部署将dist文件夹内的所有文件上传到你的Web服务器如Nginx、Apache的网站根目录下。配置Nginx推荐Nginx性能好配置简单。一个基本的配置示例如下server { listen 80; server_name your-domain.com; # 你的域名或IP # 前端静态资源 location / { root /path/to/your/dist; # dist目录的绝对路径 index index.html index.htm; try_files $uri $uri/ /index.html; # 支持Vue Router的history模式 } # 反向代理后端API location /api/ { proxy_pass http://localhost:8080/; # 后端SpringBoot应用地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这个配置做了两件事一是托管前端静态文件二是将所有以/api/开头的请求转发到后端的SpringBoot应用假设运行在8080端口完美解决了前后端分离部署的跨域问题。4.3 数据库初始化与数据备份初始化在MySQL中创建数据库如elderly_db并执行项目中的SQL脚本通常是schema.sql和data.sql来创建表结构和初始化基础数据如角色、部门、费用项目字典等。定期备份生产环境必须设置数据库自动备份。可以使用Linux的crontab定时任务执行mysqldump命令。# 编辑定时任务 crontab -e # 添加一行每天凌晨2点备份 0 2 * * * /usr/bin/mysqldump -u[用户名] -p[密码] elderly_db /backup/elderly_db_$(date \%Y\%m\%d).sql重要提示将密码写在命令行中有安全风险。更安全的方式是使用MySQL的配置文件~/.my.cnf存储凭证或在脚本中引用环境变量。5. 开发与部署中的常见问题与排查在实际开发和部署这个系统的过程中你几乎一定会遇到下面这些问题。这里我把踩过的坑和解决方案整理出来希望能帮你节省大量时间。5.1 开发环境问题问题1MyBatis-Plus 插入数据后主键ID为NULL现象调用entity.setId(null); save(entity);后实体对象的ID仍然是null但数据库里却成功生成了自增ID。原因MyBatis-Plus默认的主键生成策略是IdType.NONE即跟随数据库自增。但插入后这个自增的ID值没有回填到实体对象中。解决方案在实体类的主键字段上明确指定TableId注解的类型为IdType.AUTO并确保数据库表的主键是自增的。Data TableName(elderly) public class Elderly { TableId(type IdType.AUTO) // 关键在这里 private Long id; // ... 其他字段 }这样执行save()后实体对象的id属性就会被自动赋值为数据库生成的值。问题2前后端联调时出现跨域CORS错误现象前端浏览器控制台报错Access to fetch at ‘http://localhost:8080/api/xxx‘ from origin ‘http://localhost:3000‘ has been blocked by CORS policy。原因前端项目运行在localhost:3000后端在localhost:8080端口不同浏览器出于安全策略会阻止这种跨域请求。解决方案在后端SpringBoot应用中配置全局CORS过滤器。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) // 对所有接口 .allowedOriginPatterns(*) // 允许所有来源生产环境应替换为具体前端地址 .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意在生产环境部署时如果前后端在同一域名下通过Nginx反向代理则不会存在跨域问题此配置可移除或限制为具体域名。5.2 生产环境部署问题问题3应用启动后访问接口返回404排查步骤检查应用是否真的启动成功运行ps -ef | grep java查看进程是否存在。查看启动日志cat logs/elderly-management.log是否有异常堆栈。检查端口是否被占用运行netstat -tlnp | grep 8080你的应用端口。如果被占用需kill掉旧进程或修改application-prod.yml中的server.port。检查Nginx配置确认Nginx配置中proxy_pass的地址和端口是否正确并且后端应用确实在那个端口监听。可以尝试直接访问后端接口http://服务器IP:8080/api/xxx看是否通。检查防火墙云服务器如阿里云、腾讯云的安全组规则是否放行了8080端口后端和80/443端口前端。问题4系统运行一段时间后变慢可能原因及排查数据库连接未释放检查是否在代码中频繁创建数据库连接而未关闭。使用Druid等连接池并监控连接数。SQL查询慢对频繁查询且数据量大的表如护理记录表care_record在查询条件字段如elderly_id,execute_time上建立索引。使用EXPLAIN命令分析慢SQL。JVM内存不足或频繁GC通过启动参数增加堆内存如-Xmx2048m。使用jstat或VisualVM等工具监控GC情况。未使用缓存对于字典表、配置信息等变化不频繁的数据引入Redis缓存。在SpringBoot中可以轻松地使用Cacheable注解。5.3 业务逻辑与数据问题问题5生成账单时计算出现精度错误如0.10.2 ! 0.3原因在Java中使用float或double进行金融计算会存在精度丢失问题这是浮点数的通病。解决方案绝对不要使用Float或Double类型来存储金额必须使用BigDecimal类型并且在数据库中也对应DECIMAL类型。// 错误 double total 0.1 0.2; // total 可能是 0.30000000000000004 // 正确 BigDecimal a new BigDecimal(0.1); BigDecimal b new BigDecimal(0.2); BigDecimal total a.add(b); // total 精确等于 0.3在MyBatis-Plus的实体类中字段也应定义为BigDecimal。问题6并发操作下床位分配可能出现冲突同一床位被分配给两个老人场景两个管理员同时为不同的老人办理入住都选中了最后一个空闲床位。解决方案这是一个典型的并发更新问题。可以在业务层使用数据库的悲观锁或乐观锁。悲观锁在查询床位信息时使用SELECT ... FOR UPDATE锁定该行记录直到当前事务提交。// 在Mapper接口中定义 Select(SELECT * FROM bed WHERE id #{id} FOR UPDATE) Bed selectBedForUpdate(Long id);乐观锁在床位表中增加一个版本号字段version。更新时只有版本号匹配才更新成功。UPDATE bed SET status 已入住, version version 1 WHERE id #{id} AND version #{oldVersion} AND status 空闲;如果更新返回的影响行数为0说明床位状态已被他人修改需要提示用户重新操作。选择建议对于这种冲突概率不高但后果严重的场景使用悲观锁更简单直接。乐观锁更适合读多写少、冲突概率低的场景。这个基于SpringBoot的养老院管理系统项目从业务分析、技术选型、数据库设计、前后端实现到部署运维完整地走完了一个中小型管理系统的开发全流程。其中涉及到的RBAC权限、事务管理、前后端分离、部署配置等知识点都是企业级开发的通用技能。无论你是用于毕业设计还是作为个人学习的练手项目深入理解并实践其中的每一个环节都能为你打下坚实的全栈开发基础。在具体编码时多思考“为什么这么设计”多动手解决遇到的每一个报错你的收获会远比单纯复制一份源码大得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进